using JetBrains.Annotations;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UdonSharp;
using UdonSharp.Serialization;
using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
using VRC.Udon;
using VRC.Udon.Common.Interfaces;
using VRC.Udon.Editor.ProgramSources;
using VRC.Udon.EditorBindings;
namespace UdonSharpEditor
{
public static class UdonSharpEditorUtility
{
///
/// Creates a new UdonAssemblyProgramAsset from an UdonSharpProgramAsset for the sake of portability. Most info used for the inspector gets stripped so this isn't a great solution for remotely complex assets.
///
/// The source program asset
/// The save path for the asset file. Save path is only needed here because Udon needs a GUID for saving the serialized program asset and it'd be a pain to break that requirement at the moment
/// The exported UdonAssemblyProgramAsset
[PublicAPI]
public static UdonAssemblyProgramAsset UdonSharpProgramToAssemblyProgram(UdonSharpProgramAsset udonSharpProgramAsset, string savePath)
{
if (EditorApplication.isPlaying)
throw new System.NotSupportedException("USharpEditorUtility.UdonSharpProgramToAssemblyProgram() cannot be called in play mode");
UdonAssemblyProgramAsset newProgramAsset = ScriptableObject.CreateInstance();
AssetDatabase.CreateAsset(newProgramAsset, savePath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
newProgramAsset = AssetDatabase.LoadAssetAtPath(savePath);
udonSharpProgramAsset.CompileCsProgram();
string programAssembly = UdonSharpEditorCache.Instance.GetUASMStr(udonSharpProgramAsset);
// Strip comments/inline code
StringBuilder asmBuilder = new StringBuilder();
using (StringReader reader = new StringReader(programAssembly))
{
string line = reader.ReadLine();
while (line != null)
{
if (!string.IsNullOrWhiteSpace(line) &&
!line.TrimStart().StartsWith("#", System.StringComparison.Ordinal))
asmBuilder.AppendFormat("{0}\n", line);
line = reader.ReadLine();
}
}
programAssembly = asmBuilder.ToString();
FieldInfo assemblyField = typeof(UdonAssemblyProgramAsset).GetField("udonAssembly", BindingFlags.NonPublic | BindingFlags.Instance);
assemblyField.SetValue(newProgramAsset, programAssembly);
IUdonProgram program = null;
try
{
UdonSharp.HeapFactory heapFactory = new UdonSharp.HeapFactory();
UdonEditorInterface editorInterface = new UdonEditorInterface(null, heapFactory, null, null, null, null, null, null, null);
heapFactory.FactoryHeapSize = udonSharpProgramAsset.GetSerializedUdonProgramAsset().RetrieveProgram().Heap.GetHeapCapacity();
program = editorInterface.Assemble(programAssembly);
}
catch (System.Exception e)
{
Debug.LogError(e);
return null;
}
FieldInfo assemblyProgramField = typeof(UdonProgramAsset).GetField("program", BindingFlags.NonPublic | BindingFlags.Instance);
assemblyProgramField.SetValue(newProgramAsset, program);
IUdonProgram uSharpProgram = udonSharpProgramAsset.GetRealProgram();
IUdonProgram assemblyProgram = (IUdonProgram)assemblyProgramField.GetValue(newProgramAsset);
if (uSharpProgram == null || assemblyProgram == null)
return null;
ImmutableArray symbols = uSharpProgram.SymbolTable.GetSymbols();
foreach (string symbol in symbols)
{
uint symbolAddress = uSharpProgram.SymbolTable.GetAddressFromSymbol(symbol);
System.Type symbolType = uSharpProgram.Heap.GetHeapVariableType(symbolAddress);
object symbolValue = uSharpProgram.Heap.GetHeapVariable(symbolAddress);
assemblyProgram.Heap.SetHeapVariable(assemblyProgram.SymbolTable.GetAddressFromSymbol(symbol), symbolValue, symbolType);
}
EditorUtility.SetDirty(newProgramAsset);
newProgramAsset.SerializedProgramAsset.StoreProgram(assemblyProgram);
EditorUtility.SetDirty(newProgramAsset.SerializedProgramAsset);
AssetDatabase.SaveAssets();
// This doesn't work unfortunately due to how Udon tries to locate the serialized asset when importing an assembly
//string serializedAssetPath = $"{Path.GetDirectoryName(savePath)}/{Path.GetFileNameWithoutExtension(savePath)}_serialized.asset";
//AssetDatabase.MoveAsset(AssetDatabase.GetAssetPath(newProgramAsset.SerializedProgramAsset), serializedAssetPath);
//AssetDatabase.SaveAssets();
return newProgramAsset;
}
///
/// Deletes an UdonSharp program asset and the serialized program asset associated with it
///
///
[PublicAPI]
public static void DeleteProgramAsset(UdonSharpProgramAsset programAsset)
{
if (programAsset == null)
return;
AbstractSerializedUdonProgramAsset serializedAsset = programAsset.GetSerializedUdonProgramAsset();
if (serializedAsset != null)
{
string assetPath = AssetDatabase.GetAssetPath(serializedAsset);
serializedAsset = AssetDatabase.LoadAssetAtPath(assetPath);
if (serializedAsset != null)
{
AssetDatabase.DeleteAsset(assetPath);
}
}
string programAssetPath = AssetDatabase.GetAssetPath(programAsset);
programAsset = AssetDatabase.LoadAssetAtPath(programAssetPath);
if (programAsset != null)
AssetDatabase.DeleteAsset(programAssetPath);
}
///
/// Converts a set of UdonSharpBehaviour components to their equivalent UdonBehaviour components
///
///
///
[PublicAPI]
public static UdonBehaviour[] ConvertToUdonBehaviours(UdonSharpBehaviour[] components, bool convertChildren = false)
{
return ConvertToUdonBehavioursInternal(components, false, false, convertChildren);
}
///
/// Converts a set of UdonSharpBehaviour components to their equivalent UdonBehaviour components
/// Registers an Undo operation for the conversion
///
///
///
[PublicAPI]
public static UdonBehaviour[] ConvertToUdonBehavioursWithUndo(UdonSharpBehaviour[] components, bool convertChildren = false)
{
return ConvertToUdonBehavioursInternal(components, true, false, convertChildren);
}
static internal Dictionary _programAssetLookup;
static internal Dictionary _programAssetTypeLookup;
private static void InitTypeLookups()
{
if (_programAssetLookup == null)
{
_programAssetLookup = new Dictionary();
_programAssetTypeLookup = new Dictionary();
UdonSharpProgramAsset[] udonSharpProgramAssets = UdonSharpProgramAsset.GetAllUdonSharpPrograms();
foreach (UdonSharpProgramAsset programAsset in udonSharpProgramAssets)
{
if (programAsset && programAsset.sourceCsScript != null && !_programAssetLookup.ContainsKey(programAsset.sourceCsScript))
{
_programAssetLookup.Add(programAsset.sourceCsScript, programAsset);
if (programAsset.GetClass() != null)
_programAssetTypeLookup.Add(programAsset.GetClass(), programAsset);
}
}
}
}
private static UdonSharpProgramAsset GetUdonSharpProgramAsset(MonoScript programScript)
{
InitTypeLookups();
_programAssetLookup.TryGetValue(programScript, out UdonSharpProgramAsset foundProgramAsset);
return foundProgramAsset;
}
///
/// Gets the UdonSharpProgramAsset that represents the program for the given UdonSharpBehaviour
///
///
///
[PublicAPI]
public static UdonSharpProgramAsset GetUdonSharpProgramAsset(UdonSharpBehaviour udonSharpBehaviour)
{
return GetUdonSharpProgramAsset(MonoScript.FromMonoBehaviour(udonSharpBehaviour));
}
[PublicAPI]
public static UdonSharpProgramAsset GetUdonSharpProgramAsset(System.Type type)
{
InitTypeLookups();
_programAssetTypeLookup.TryGetValue(type, out UdonSharpProgramAsset foundProgramAsset);
return foundProgramAsset;
}
private static readonly FieldInfo _backingBehaviourField = typeof(UdonSharpBehaviour).GetField("_backingUdonBehaviour", BindingFlags.NonPublic | BindingFlags.Instance);
///
/// Gets the backing UdonBehaviour for a proxy
///
///
///
[PublicAPI]
public static UdonBehaviour GetBackingUdonBehaviour(UdonSharpBehaviour behaviour)
{
return (UdonBehaviour)_backingBehaviourField.GetValue(behaviour);
}
internal static void SetBackingUdonBehaviour(UdonSharpBehaviour behaviour, UdonBehaviour backingBehaviour)
{
_backingBehaviourField.SetValue(behaviour, backingBehaviour);
}
///
/// Returns true if the given behaviour is a proxy behaviour that's linked to an UdonBehaviour.
///
///
///
[PublicAPI]
public static bool IsProxyBehaviour(UdonSharpBehaviour behaviour)
{
return GetBackingUdonBehaviour(behaviour) != null;
}
static Dictionary _proxyBehaviourLookup = new Dictionary();
///
/// Finds an existing proxy behaviour, if none exists returns null
///
///
///
[PublicAPI]
public static UdonSharpBehaviour FindProxyBehaviour(UdonBehaviour udonBehaviour)
{
return FindProxyBehaviour(udonBehaviour, ProxySerializationPolicy.Default);
}
///
/// Finds an existing proxy behaviour, if none exists returns null
///
///
///
///
[PublicAPI]
public static UdonSharpBehaviour FindProxyBehaviour(UdonBehaviour udonBehaviour, ProxySerializationPolicy proxySerializationPolicy)
{
if (_proxyBehaviourLookup.TryGetValue(udonBehaviour, out UdonSharpBehaviour proxyBehaviour))
{
if (proxyBehaviour != null)
{
CopyUdonToProxy(proxyBehaviour, proxySerializationPolicy);
SetIgnoreEvents(true);
try
{
proxyBehaviour.enabled = false;
}
finally
{
SetIgnoreEvents(false);
}
return proxyBehaviour;
}
else
{
_proxyBehaviourLookup.Remove(udonBehaviour);
}
}
UdonSharpBehaviour[] behaviours = udonBehaviour.GetComponents();
foreach (UdonSharpBehaviour udonSharpBehaviour in behaviours)
{
IUdonBehaviour backingBehaviour = GetBackingUdonBehaviour(udonSharpBehaviour);
if (backingBehaviour != null && ReferenceEquals(backingBehaviour, udonBehaviour))
{
_proxyBehaviourLookup.Add(udonBehaviour, udonSharpBehaviour);
CopyUdonToProxy(udonSharpBehaviour, proxySerializationPolicy);
SetIgnoreEvents(true);
try
{
udonSharpBehaviour.enabled = false;
}
finally
{
SetIgnoreEvents(false);
}
return udonSharpBehaviour;
}
}
return null;
}
///
/// Gets the C# version of an UdonSharpBehaviour that proxies an UdonBehaviour with the program asset for the matching UdonSharpBehaviour type
///
///
///
[PublicAPI]
public static UdonSharpBehaviour GetProxyBehaviour(UdonBehaviour udonBehaviour)
{
return GetProxyBehaviour(udonBehaviour, ProxySerializationPolicy.Default);
}
///
/// Returns if the given UdonBehaviour is an UdonSharpBehaviour
///
///
///
[PublicAPI]
public static bool IsUdonSharpBehaviour(UdonBehaviour udonBehaviour)
{
return udonBehaviour.programSource != null &&
udonBehaviour.programSource is UdonSharpProgramAsset programAsset &&
programAsset.sourceCsScript != null;
}
///
/// Gets the UdonSharpBehaviour type from the given behaviour.
/// If the behaviour is not an UdonSharpBehaviour, returns null.
///
///
///
[PublicAPI]
public static System.Type GetUdonSharpBehaviourType(UdonBehaviour udonBehaviour)
{
if (!IsUdonSharpBehaviour(udonBehaviour))
return null;
return ((UdonSharpProgramAsset)udonBehaviour.programSource).GetClass();
}
static FieldInfo _skipEventsField = null;
///
/// Used to disable sending events to UdonSharpBehaviours for OnEnable, OnDisable, and OnDestroy since they are not always in a valid state to be recognized as proxies during these events.
///
///
internal static void SetIgnoreEvents(bool ignore)
{
if (_skipEventsField == null)
_skipEventsField = typeof(UdonSharpBehaviour).GetField("_skipEvents", BindingFlags.Static | BindingFlags.NonPublic);
_skipEventsField.SetValue(null, ignore);
}
///
/// Gets the C# version of an UdonSharpBehaviour that proxies an UdonBehaviour with the program asset for the matching UdonSharpBehaviour type
///
///
///
///
[PublicAPI]
public static UdonSharpBehaviour GetProxyBehaviour(UdonBehaviour udonBehaviour, ProxySerializationPolicy proxySerializationPolicy)
{
if (udonBehaviour == null)
throw new System.ArgumentNullException("Source Udon Behaviour cannot be null");
if (udonBehaviour.programSource == null)
throw new System.ArgumentNullException("Program source on UdonBehaviour cannot be null");
UdonSharpProgramAsset udonSharpProgram = udonBehaviour.programSource as UdonSharpProgramAsset;
if (udonSharpProgram == null)
throw new System.ArgumentException("UdonBehaviour must be using an UdonSharp program");
UdonSharpBehaviour proxyBehaviour = FindProxyBehaviour(udonBehaviour, proxySerializationPolicy);
if (proxyBehaviour)
return proxyBehaviour;
// We've failed to find an existing proxy behaviour so we need to create one
System.Type scriptType = udonSharpProgram.GetClass();
if (scriptType == null)
return null;
SetIgnoreEvents(true);
try
{
proxyBehaviour = (UdonSharpBehaviour)udonBehaviour.gameObject.AddComponent(scriptType);
proxyBehaviour.hideFlags = HideFlags.DontSaveInBuild |
#if !UDONSHARP_DEBUG
HideFlags.HideInInspector |
#endif
HideFlags.DontSaveInEditor;
proxyBehaviour.enabled = false;
}
finally
{
SetIgnoreEvents(false);
}
SetBackingUdonBehaviour(proxyBehaviour, udonBehaviour);
_proxyBehaviourLookup.Add(udonBehaviour, proxyBehaviour);
CopyUdonToProxy(proxyBehaviour, proxySerializationPolicy);
return proxyBehaviour;
}
///
/// Copies the state of the proxy to its backing UdonBehaviour
///
///
[PublicAPI]
public static void CopyProxyToUdon(UdonSharpBehaviour proxy)
{
CopyProxyToUdon(proxy, ProxySerializationPolicy.Default);
}
///
/// Copies the state of the UdonBehaviour to its proxy object
///
///
[PublicAPI]
public static void CopyUdonToProxy(UdonSharpBehaviour proxy)
{
CopyUdonToProxy(proxy, ProxySerializationPolicy.Default);
}
///
/// Copies the state of the proxy to its backing UdonBehaviour
///
///
///
[PublicAPI]
public static void CopyProxyToUdon(UdonSharpBehaviour proxy, ProxySerializationPolicy serializationPolicy)
{
if (serializationPolicy.MaxSerializationDepth == 0)
return;
Profiler.BeginSample("CopyProxyToUdon");
SimpleValueStorage udonBehaviourStorage = new SimpleValueStorage(GetBackingUdonBehaviour(proxy));
ProxySerializationPolicy lastPolicy = USBSerializationContext.currentPolicy;
USBSerializationContext.currentPolicy = serializationPolicy;
Serializer.CreatePooled(proxy.GetType()).WriteWeak(udonBehaviourStorage, proxy);
USBSerializationContext.currentPolicy = lastPolicy;
Profiler.EndSample();
}
///
/// Copies the state of the UdonBehaviour to its proxy object
///
///
///
[PublicAPI]
public static void CopyUdonToProxy(UdonSharpBehaviour proxy, ProxySerializationPolicy serializationPolicy)
{
if (serializationPolicy.MaxSerializationDepth == 0)
return;
Profiler.BeginSample("CopyUdonToProxy");
SimpleValueStorage udonBehaviourStorage = new SimpleValueStorage(GetBackingUdonBehaviour(proxy));
ProxySerializationPolicy lastPolicy = USBSerializationContext.currentPolicy;
USBSerializationContext.currentPolicy = serializationPolicy;
object proxyObj = proxy;
Serializer.CreatePooled(proxy.GetType()).ReadWeak(ref proxyObj, udonBehaviourStorage);
USBSerializationContext.currentPolicy = lastPolicy;
Profiler.EndSample();
}
[PublicAPI]
public static UdonBehaviour CreateBehavourForProxy(UdonSharpBehaviour udonSharpBehaviour)
{
UdonBehaviour backingBehaviour = GetBackingUdonBehaviour(udonSharpBehaviour);
if (backingBehaviour == null)
{
backingBehaviour = udonSharpBehaviour.gameObject.AddComponent();
backingBehaviour.programSource = GetUdonSharpProgramAsset(udonSharpBehaviour);
}
CopyProxyToUdon(udonSharpBehaviour);
return backingBehaviour;
}
///
/// Destroys an UdonSharpBehaviour proxy and its underlying UdonBehaviour
///
///
[PublicAPI]
public static void DestroyImmediate(UdonSharpBehaviour behaviour)
{
UdonBehaviour backingBehaviour = GetBackingUdonBehaviour(behaviour);
Object.DestroyImmediate(behaviour);
if (backingBehaviour)
{
_proxyBehaviourLookup.Remove(backingBehaviour);
SetIgnoreEvents(true);
try
{
Object.DestroyImmediate(backingBehaviour);
}
finally
{
SetIgnoreEvents(false);
}
}
}
#region Internal utilities
internal static void CollectUdonSharpBehaviourReferencesInternal(object rootObject, HashSet gatheredSet, HashSet