using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UdonSharp;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using VRC.Udon;
using VRC.Udon.Editor;
#if ODIN_INSPECTOR_3
using UdonSharpEditor;
using Sirenix.OdinInspector.Editor;
[assembly: DefaultUdonSharpBehaviourEditor(typeof(OdinInspectorHandler), "Odin Inspector")]
#endif
///
/// Example use of how to register a default inspector
///
#if false
using UdonSharpEditor;
[assembly:DefaultUdonSharpBehaviourEditor(typeof(DemoDefaultBehaviourEditor), "UdonSharp Demo Inspector")]
#endif
namespace UdonSharpEditor
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public class DefaultUdonSharpBehaviourEditorAttribute : Attribute
{
internal System.Type inspectorType;
internal string inspectorDisplayName;
public DefaultUdonSharpBehaviourEditorAttribute(System.Type inspectorType, string inspectorDisplayName)
{
this.inspectorType = inspectorType;
this.inspectorDisplayName = inspectorDisplayName;
}
}
///
/// Basic demo inspector that just draws fields using the Unity handling. Not intended to be used.
///
internal class DemoDefaultBehaviourEditor : Editor
{
public override void OnInspectorGUI()
{
if (UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target, false, false)) return;
base.OnInspectorGUI();
}
}
#if ODIN_INSPECTOR_3
internal class OdinInspectorHandler : OdinEditor
{
public override void OnInspectorGUI()
{
if (UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target)) return;
DrawTree();
}
}
#endif
[CustomEditor(typeof(UdonSharpBehaviour), true)]
internal class UdonSharpBehaviourEditor : Editor
{
[MenuItem("Assets/Create/U# Script", false, 5)]
private static void CreateUSharpScript()
{
string folderPath = "Assets/";
if (Selection.activeObject != null)
{
folderPath = AssetDatabase.GetAssetPath(Selection.activeObject);
if (Selection.activeObject.GetType() != typeof(UnityEditor.DefaultAsset))
{
folderPath = Path.GetDirectoryName(folderPath);
}
}
else if (Selection.assetGUIDs.Length > 0)
{
folderPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]);
}
folderPath = folderPath.Replace('\\', '/');
string chosenFilePath = EditorUtility.SaveFilePanelInProject("Save UdonSharp File", "", "cs", "Save UdonSharp file", folderPath);
if (chosenFilePath.Length > 0)
{
chosenFilePath = UdonSharpSettings.SanitizeScriptFilePath(chosenFilePath);
string chosenFileName = Path.GetFileNameWithoutExtension(chosenFilePath).Replace(" ", "").Replace("#", "Sharp");
string assetFilePath = Path.Combine(Path.GetDirectoryName(chosenFilePath), $"{chosenFileName}.asset");
if (AssetDatabase.LoadAssetAtPath(assetFilePath) != null)
{
if (!EditorUtility.DisplayDialog("File already exists", $"Corresponding asset file '{assetFilePath}' already found for new UdonSharp script. Overwrite?", "Ok", "Cancel"))
return;
}
string fileContents = UdonSharpSettings.GetProgramTemplateString(chosenFileName);
File.WriteAllText(chosenFilePath, fileContents, System.Text.Encoding.UTF8);
AssetDatabase.ImportAsset(chosenFilePath, ImportAssetOptions.ForceSynchronousImport);
MonoScript newScript = AssetDatabase.LoadAssetAtPath(chosenFilePath);
UdonSharpProgramAsset newProgramAsset = ScriptableObject.CreateInstance();
newProgramAsset.sourceCsScript = newScript;
AssetDatabase.CreateAsset(newProgramAsset, assetFilePath);
AssetDatabase.Refresh();
}
}
public override void OnInspectorGUI()
{
if (UdonSharpGUI.DrawConvertToUdonBehaviourButton(targets))
return;
base.OnInspectorGUI();
}
}
#region Drawer override boilerplate
[InitializeOnLoad]
internal class UdonBehaviourDrawerOverride
{
static UdonBehaviourDrawerOverride()
{
OverrideUdonBehaviourDrawer();
}
static FieldInfo customEditorField;
static MethodInfo removeTypeMethod;
static MethodInfo addTypeMethod;
static System.Type monoEditorTypeType;
static System.Type monoEditorTypeListType;
static MethodInfo listAddTypeMethod;
static MethodInfo listClearMethod;
static FieldInfo monoEditorTypeInspectedTypeField;
static FieldInfo monoEditorTypeInspectorTypeField;
static readonly object[] udonBehaviourTypeArr = new object[] { typeof(UdonBehaviour) };
static readonly object[] addTypeInvokeParams = new object[] { typeof(UdonBehaviour), null };
static readonly object[] listCreateParams = new object[] { 1 };
static object customEditorDictionary;
static object editorTypeList;
static object editorTypeObject;
///
/// Handles removing the reference to the default UdonBehaviourEditor and injecting our own custom editor UdonBehaviourOverrideEditor
///
public static void OverrideUdonBehaviourDrawer()
{
if (customEditorField == null)
{
Assembly editorAssembly = typeof(UnityEditor.Editor).Assembly;
System.Type editorAttributesClass = editorAssembly.GetType("UnityEditor.CustomEditorAttributes");
customEditorField = editorAttributesClass.GetField("kSCustomEditors", BindingFlags.NonPublic | BindingFlags.Static);
System.Type fieldType = customEditorField.FieldType;
removeTypeMethod = fieldType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(e => e.Name == "Remove" &&
e.GetParameters().Length == 1 &&
e.GetParameters()[0].ParameterType == typeof(System.Type));
monoEditorTypeType = editorAttributesClass.GetNestedType("MonoEditorType", BindingFlags.NonPublic);
monoEditorTypeInspectedTypeField = monoEditorTypeType.GetField("m_InspectedType", BindingFlags.Public | BindingFlags.Instance);
monoEditorTypeInspectorTypeField = monoEditorTypeType.GetField("m_InspectorType", BindingFlags.Public | BindingFlags.Instance);
monoEditorTypeListType = typeof(List<>).MakeGenericType(monoEditorTypeType);
addTypeMethod = fieldType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(e => e.Name == "Add" &&
e.GetParameters().Length == 2 &&
e.GetParameters()[0].ParameterType == typeof(System.Type) &&
e.GetParameters()[1].ParameterType == monoEditorTypeListType);
listAddTypeMethod = monoEditorTypeListType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(e => e.Name == "Add" &&
e.GetParameters().Length == 1 &&
e.GetParameters()[0].ParameterType == monoEditorTypeType);
listClearMethod = monoEditorTypeListType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(e => e.Name == "Clear" &&
e.GetParameters().Length == 0);
customEditorDictionary = customEditorField.GetValue(null);
editorTypeObject = Activator.CreateInstance(monoEditorTypeType);
monoEditorTypeInspectedTypeField.SetValue(editorTypeObject, typeof(UdonBehaviour));
monoEditorTypeInspectorTypeField.SetValue(editorTypeObject, typeof(UdonBehaviourOverrideEditor));
editorTypeList = Activator.CreateInstance(monoEditorTypeListType);
listCreateParams[0] = editorTypeObject;
}
listClearMethod.Invoke(editorTypeList, null);
listAddTypeMethod.Invoke(editorTypeList, listCreateParams);
removeTypeMethod.Invoke(customEditorDictionary, udonBehaviourTypeArr);
addTypeInvokeParams[1] = editorTypeList;
addTypeMethod.Invoke(customEditorDictionary, addTypeInvokeParams);
}
}
#endregion
#region Editor Manager
[InitializeOnLoad]
internal static class UdonSharpCustomEditorManager
{
static Dictionary _typeInspectorMap;
internal static Dictionary _defaultInspectorMap;
static UdonSharpCustomEditorManager()
{
InitInspectorMap();
Undo.postprocessModifications += OnPostprocessUndoModifications;
}
static void InitInspectorMap()
{
_typeInspectorMap = new Dictionary();
_defaultInspectorMap = new Dictionary();
FieldInfo inspectedTypeField = typeof(CustomEditor).GetField("m_InspectedType", BindingFlags.NonPublic | BindingFlags.Instance);
foreach (Assembly asm in UdonSharpUtils.GetLoadedEditorAssemblies())
{
foreach (Type editorType in asm.GetTypes())
{
IEnumerable editorAttributes = editorType.GetCustomAttributes();
foreach (CustomEditor editorAttribute in editorAttributes)
{
if (editorAttribute != null && editorAttribute.GetType() == typeof(CustomEditor)) // The CustomEditorForRenderPipeline attribute inherits from CustomEditor, but we do not want to take that into account.
{
Type inspectedType = (Type)inspectedTypeField.GetValue(editorAttribute);
if (inspectedType.IsSubclassOf(typeof(UdonSharpBehaviour)))
{
if (_typeInspectorMap.ContainsKey(inspectedType))
{
Debug.LogError($"Cannot register inspector '{editorType.Name}' for type '{inspectedType.Name}' since inspector '{_typeInspectorMap[inspectedType].Name}' is already registered");
continue;
}
_typeInspectorMap.Add(inspectedType, editorType);
}
}
}
}
foreach (DefaultUdonSharpBehaviourEditorAttribute editorAttribute in asm.GetCustomAttributes())
{
if (!editorAttribute.inspectorType.IsSubclassOf(typeof(Editor)))
{
Debug.LogError($"Could not add default inspector '{editorAttribute.inspectorType}', custom inspectors must inherit from UnityEditor.Editor");
continue;
}
_defaultInspectorMap.Add(editorAttribute.inspectorType.ToString(), (editorAttribute.inspectorDisplayName, editorAttribute.inspectorType));
}
}
}
///
/// Dirties the underlying UdonBehaviour when a proxy UdonSharpBehaviour is modified since Unity does not mark the scene dirty when a modified object is marked 'HideFlags.DontSaveInEditor'
/// Has the downside that the scene will not be "undirtied" when a change that dirtied it is undone.
///
///
///
static UndoPropertyModification[] OnPostprocessUndoModifications(UndoPropertyModification[] propertyModifications)
{
if (!EditorApplication.isPlaying)
{
HashSet modifiedBehaviours = new HashSet();
foreach (UndoPropertyModification propertyModification in propertyModifications)
{
UnityEngine.Object target = propertyModification.currentValue?.target;
if (target != null && target is UdonSharpBehaviour udonSharpBehaviour)
{
UdonBehaviour backingBehaviour = UdonSharpEditorUtility.GetBackingUdonBehaviour(udonSharpBehaviour);
if (backingBehaviour)
{
modifiedBehaviours.Add(backingBehaviour);
}
}
}
if (modifiedBehaviours.Count > 0)
{
foreach (UdonBehaviour behaviour in modifiedBehaviours)
{
if (PrefabUtility.IsPartOfPrefabInstance(behaviour))
PrefabUtility.RecordPrefabInstancePropertyModifications(behaviour);
}
EditorSceneManager.MarkAllScenesDirty();
}
}
return propertyModifications;
}
public static System.Type GetInspectorEditorType(System.Type udonSharpBehaviourType)
{
System.Type editorType;
_typeInspectorMap.TryGetValue(udonSharpBehaviourType, out editorType);
if (editorType == null)
{
UdonSharpSettings settings = UdonSharpSettings.GetSettings();
if (settings)
{
string defaultEditor = settings.defaultBehaviourInterfaceType;
if (!string.IsNullOrEmpty(defaultEditor))
{
if (_defaultInspectorMap.TryGetValue(defaultEditor, out var defaultEditorType))
{
editorType = defaultEditorType.Item2;
}
}
}
}
return editorType;
}
}
#endregion
///
/// Custom U# editor for UdonBehaviours that can have custom behavior for drawing stuff like sync position and the program asset info
/// Will also allow people to override the inspector for their own custom inspectors
///
internal class UdonBehaviourOverrideEditor : Editor
{
Editor baseEditor;
UdonSharpBehaviour currentProxyBehaviour;
private void OnEnable()
{
UdonEditorManager.Instance.WantRepaint += Repaint;
if (target && PrefabUtility.IsPartOfPrefabAsset(target))
return;
Undo.undoRedoPerformed += OnUndoRedo;
if (target is UdonBehaviour udonBehaviour && UdonSharpEditorUtility.IsUdonSharpBehaviour(udonBehaviour))
{
UdonSharpBehaviour proxyBehaviour = UdonSharpEditorUtility.GetProxyBehaviour(udonBehaviour, ProxySerializationPolicy.NoSerialization);
if (proxyBehaviour)
proxyBehaviour.hideFlags =
HideFlags.DontSaveInBuild |
#if !UDONSHARP_DEBUG
HideFlags.HideInInspector |
#endif
HideFlags.DontSaveInEditor;
}
}
private void OnDisable()
{
UdonEditorManager.Instance.WantRepaint -= Repaint;
if (target && PrefabUtility.IsPartOfPrefabAsset(target))
return;
Undo.undoRedoPerformed -= OnUndoRedo;
}
void OnUndoRedo()
{
UdonBehaviour behaviour = target as UdonBehaviour;
UdonSharpBehaviour inspectorTarget = UdonSharpEditorUtility.FindProxyBehaviour(behaviour, ProxySerializationPolicy.NoSerialization);
if (inspectorTarget)
{
System.Type customEditorType = UdonSharpCustomEditorManager.GetInspectorEditorType(inspectorTarget.GetType());
if (customEditorType != null) // Only do the undo copying on things with a custom inspector
{
if ((bool)typeof(UdonSharpBehaviour).GetField("_isValidForAutoCopy", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(inspectorTarget))
{
UdonSharpEditorUtility.CopyProxyToUdon(inspectorTarget, ProxySerializationPolicy.All);
if (PrefabUtility.IsPartOfPrefabInstance(behaviour))
PrefabUtility.RecordPrefabInstancePropertyModifications(behaviour);
}
}
}
}
private void OnDestroy()
{
if (baseEditor)
DestroyImmediate(baseEditor);
CleanupProxy();
}
void CleanupProxy()
{
if (currentProxyBehaviour != null)
{
UdonBehaviour backingBehaviour = UdonSharpEditorUtility.GetBackingUdonBehaviour(currentProxyBehaviour);
if (backingBehaviour == null)
{
UdonSharpEditorUtility.SetIgnoreEvents(true);
try
{
UnityEngine.Object.DestroyImmediate(currentProxyBehaviour);
}
finally
{
UdonSharpEditorUtility.SetIgnoreEvents(false);
}
}
}
}
static FieldInfo _autoCopyValidField = null;
public override void OnInspectorGUI()
{
UdonBehaviour behaviour = target as UdonBehaviour;
// Fall back to the default Udon inspector if not a U# behaviour
if (behaviour.programSource == null || !(behaviour.programSource is UdonSharpProgramAsset udonSharpProgram))
{
if (!baseEditor)
Editor.CreateCachedEditorWithContext(targets, this, typeof(UdonBehaviourEditor), ref baseEditor);
baseEditor.OnInspectorGUI();
return;
}
UdonSharpProgramAsset programAsset = (UdonSharpProgramAsset)behaviour.programSource;
programAsset.UpdateProgram();
System.Type customEditorType = null;
System.Type inspectedType = programAsset.GetClass();
if (inspectedType != null)
customEditorType = UdonSharpCustomEditorManager.GetInspectorEditorType(inspectedType);
if (customEditorType != null && !PrefabUtility.IsPartOfPrefabAsset(target))
{
if (baseEditor != null && baseEditor.GetType() != customEditorType)
{
DestroyImmediate(baseEditor);
baseEditor = null;
}
UdonSharpBehaviour inspectorTarget = UdonSharpEditorUtility.GetProxyBehaviour(behaviour, ProxySerializationPolicy.All);
inspectorTarget.enabled = false;
if (_autoCopyValidField == null)
_autoCopyValidField = typeof(UdonSharpBehaviour).GetField("_isValidForAutoCopy", BindingFlags.NonPublic | BindingFlags.Instance);
_autoCopyValidField.SetValue(inspectorTarget, true);
Editor.CreateCachedEditorWithContext(inspectorTarget, this, customEditorType, ref baseEditor);
currentProxyBehaviour = inspectorTarget;
baseEditor.serializedObject.Update();
baseEditor.OnInspectorGUI();
UdonSharpEditorUtility.CopyProxyToUdon(inspectorTarget, ProxySerializationPolicy.All);
}
else
{
// Create a proxy behaviour so that other things can find this object
if (programAsset.sourceCsScript != null && !PrefabUtility.IsPartOfPrefabAsset(target))
{
currentProxyBehaviour = UdonSharpEditorUtility.GetProxyBehaviour(behaviour, ProxySerializationPolicy.NoSerialization);
if (currentProxyBehaviour)
currentProxyBehaviour.enabled = false;
}
DrawDefaultUdonSharpInspector();
}
}
private void OnSceneGUI()
{
if (PrefabUtility.IsPartOfPrefabAsset(target))
return;
UdonBehaviour behaviour = target as UdonBehaviour;
if (behaviour.programSource == null ||
!(behaviour.programSource is UdonSharpProgramAsset udonSharpProgram) ||
udonSharpProgram.sourceCsScript == null)
return;
System.Type customEditorType = null;
System.Type inspectedType = udonSharpProgram.GetClass();
if (inspectedType != null)
customEditorType = UdonSharpCustomEditorManager.GetInspectorEditorType(inspectedType);
if (customEditorType == null)
return;
MethodInfo onSceneGUIMethod = customEditorType.GetMethod("OnSceneGUI", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null);
if (onSceneGUIMethod == null)
return;
udonSharpProgram.UpdateProgram();
if (baseEditor != null && baseEditor.GetType() != customEditorType)
{
DestroyImmediate(baseEditor);
baseEditor = null;
}
UdonSharpBehaviour inspectorTarget = UdonSharpEditorUtility.GetProxyBehaviour(behaviour, ProxySerializationPolicy.All);
inspectorTarget.enabled = false;
Editor.CreateCachedEditorWithContext(inspectorTarget, this, customEditorType, ref baseEditor);
baseEditor.serializedObject.Update();
onSceneGUIMethod.Invoke(baseEditor, null);
UdonSharpEditorUtility.CopyProxyToUdon(inspectorTarget, ProxySerializationPolicy.All);
}
void DrawDefaultUdonSharpInspector()
{
UdonBehaviour behaviour = target as UdonBehaviour;
if (UdonSharpGUI.DrawProgramSource(behaviour))
return;
UdonSharpGUI.DrawSyncSettings(behaviour);
UdonSharpGUI.DrawInteractSettings(behaviour);
UdonSharpProgramAsset udonSharpProgramAsset = (UdonSharpProgramAsset)behaviour.programSource;
UdonSharpGUI.DrawUtilities(behaviour, udonSharpProgramAsset);
UdonSharpGUI.DrawUILine();
udonSharpProgramAsset.DrawErrorTextAreas();
bool dirty = false;
UdonSharpGUI.DrawPublicVariables(behaviour, udonSharpProgramAsset, ref dirty);
}
public override bool RequiresConstantRepaint()
{
// Force repaint for variable update in play mode
bool requiresConstantRepaintDefaultReturnValue = Application.isPlaying;
if (PrefabUtility.IsPartOfPrefabAsset(target))
return requiresConstantRepaintDefaultReturnValue;
UdonBehaviour behaviour = target as UdonBehaviour;
if (behaviour.programSource == null ||
!(behaviour.programSource is UdonSharpProgramAsset udonSharpProgram) ||
udonSharpProgram.sourceCsScript == null)
return requiresConstantRepaintDefaultReturnValue;
System.Type customEditorType = null;
System.Type inspectedType = udonSharpProgram.GetClass();
if (inspectedType != null)
customEditorType = UdonSharpCustomEditorManager.GetInspectorEditorType(inspectedType);
if (customEditorType == null)
return requiresConstantRepaintDefaultReturnValue;
MethodInfo requiresConstantRepaintMethod = customEditorType.GetMethod("RequiresConstantRepaint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null);
if (requiresConstantRepaintMethod == null)
return requiresConstantRepaintDefaultReturnValue;
udonSharpProgram.UpdateProgram();
if (baseEditor != null && baseEditor.GetType() != customEditorType)
{
DestroyImmediate(baseEditor);
baseEditor = null;
}
UdonSharpBehaviour inspectorTarget = UdonSharpEditorUtility.GetProxyBehaviour(behaviour, ProxySerializationPolicy.All);
inspectorTarget.enabled = false;
Editor.CreateCachedEditorWithContext(inspectorTarget, this, customEditorType, ref baseEditor);
baseEditor.serializedObject.Update();
bool requiresConstantRepaintReturnValue = (bool)requiresConstantRepaintMethod.Invoke(baseEditor, null);
UdonSharpEditorUtility.CopyProxyToUdon(inspectorTarget, ProxySerializationPolicy.All);
return requiresConstantRepaintReturnValue;
}
}
}