diff options
Diffstat (limited to 'VRCSDK3Worlds/Assets/Udon/UdonBehaviour.cs')
| -rw-r--r-- | VRCSDK3Worlds/Assets/Udon/UdonBehaviour.cs | 1629 |
1 files changed, 1629 insertions, 0 deletions
diff --git a/VRCSDK3Worlds/Assets/Udon/UdonBehaviour.cs b/VRCSDK3Worlds/Assets/Udon/UdonBehaviour.cs new file mode 100644 index 00000000..e002eed7 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/UdonBehaviour.cs @@ -0,0 +1,1629 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using JetBrains.Annotations; +using Unity.Profiling; +using UnityEngine; +using VRC.SDK3.Components; +using VRC.SDKBase; +using VRC.Udon.Common; +using VRC.Udon.Common.Attributes; +using VRC.Udon.Common.Enums; +using VRC.Udon.Common.Interfaces; +using VRC.Udon.Serialization.OdinSerializer; +using VRC.Udon.VM; +using Logger = VRC.Core.Logger; +using Object = UnityEngine.Object; + +#if UNITY_EDITOR && !VRC_CLIENT +using UnityEditor.SceneManagement; +#endif + +namespace VRC.Udon +{ + public sealed class UdonBehaviour : AbstractUdonBehaviour, ISerializationCallbackReceiver + { + #region Odin Serialized Fields + + [PublicAPI] + public IUdonVariableTable publicVariables = new UdonVariableTable(); + + #endregion + + #region Serialized Public Fields + + [Obsolete("Use VRCObjectSync instead")] + [PublicAPI] + // ReSharper disable once InconsistentNaming + public bool SynchronizePosition; + + // ReSharper disable once InconsistentNaming + [PublicAPI] + public readonly bool SynchronizeAnimation = false; //We don't support animation sync yet, coming soon. + + // ReSharper disable once InconsistentNaming + + [Obsolete("Use VRCObjectSync instead")] + [PublicAPI] + public bool AllowCollisionOwnershipTransfer = true; + + // ReSharper disable once InconsistentNaming + [HideInInspector, Obsolete("Use SyncMethod instead")] + public bool Reliable = false; + + // ReSharper disable once InconsistentNaming + [SerializeField] + private Networking.SyncType _syncMethod = Networking.SyncType.Unknown; + + public Networking.SyncType SyncMethod + { + get + { + // Old Scene? + if(_syncMethod == Networking.SyncType.Unknown) + { +#pragma warning disable 618 + _syncMethod = Reliable ? Networking.SyncType.Manual : Networking.SyncType.Continuous; +#pragma warning restore 618 + } + + return _syncMethod; + } + set + { + _syncMethod = value; + if(value == Networking.SyncType.None) + { + return; + } + + // All synced UdonBehaviours on one GameObject must use the same sync method. + foreach(UdonBehaviour ub in gameObject.GetComponents<UdonBehaviour>()) + { + if(ub != null && ub._syncMethod != Networking.SyncType.None) + { + ub._syncMethod = value; + } + } + } + } + + public bool SyncIsContinuous => SyncMethod == Networking.SyncType.Continuous; + public bool SyncIsManual => SyncMethod == Networking.SyncType.Manual; + + #endregion + + #region Serialized Private Fields + + [SerializeField] + private AbstractSerializedUdonProgramAsset serializedProgramAsset; + + #if UNITY_EDITOR && !VRC_CLIENT + [SerializeField] + public AbstractUdonProgramSource programSource; + + #endif + + #endregion + + #region Public Fields and Properties + + [PublicAPI] + public static Action<UdonBehaviour, IUdonProgram> OnInit { get; set; } = null; + + [PublicAPI] + public static Action<UdonBehaviour> RequestSerializationHook { get; set; } = null; + + [PublicAPI] + public static Action<UdonBehaviour, NetworkEventTarget, string> SendCustomNetworkEventHook { get; set; } = null; + + [PublicAPI] + public override bool DisableInteractive { get; set; } + + [PublicAPI] + [ExcludeFromUdonWrapper] + public override bool IsNetworkingSupported + { + get => _isNetworkingSupported; + set + { + if(_initialized) + { + throw new InvalidOperationException( + "IsNetworkingSupported cannot be changed after the UdonBehaviour has been initialized."); + } + + _isNetworkingSupported = value; + } + } + + public override bool IsInteractive => _hasInteractiveEvents && !DisableInteractive; + + // ReSharper disable once InconsistentNaming + public const string ReturnVariableName = "__returnValue"; + + internal int UpdateOrder => _program?.UpdateOrder ?? 0; + + #endregion + + #region Private Fields and Properties + + private UdonManager _udonManager; + private IUdonProgram _program; + private IUdonVM _udonVM; + private bool _isReady; + private int _debugLevel; + private bool _hasError; + private bool _hasDoneStart; + private bool _initialized; + private bool _isNetworkingSupported = false; + + private bool _hasInteractiveEvents; + private bool _hasUpdateEvent; + private bool _hasLateUpdateEvent; + private bool _hasFixedUpdateEvent; + private bool _hasPostLateUpdateEvent; + private readonly Dictionary<string, List<uint>> _eventTable = new Dictionary<string, List<uint>>(); + + private readonly Dictionary<(string eventName, string symbolName), string> _symbolNameCache = + new Dictionary<(string, string), string>(); + + private static ProfilerMarker _managedUpdateProfilerMarker = + new ProfilerMarker("UdonBehaviour.ManagedUpdate()"); + + private static ProfilerMarker _managedLateUpdateProfilerMarker = + new ProfilerMarker("UdonBehaviour.ManagedLateUpdate()"); + + private static ProfilerMarker _managedFixedUpdateProfilerMarker = + new ProfilerMarker("UdonBehaviour.ManagedFixedUpdate()"); + + private static ProfilerMarker _postLateUpdateProfilerMarker = + new ProfilerMarker("UdonBehaviour.PostLateUpdate()"); + + private readonly SortedDictionary<uint, (uint, uint)> _variableToChangeEvent = new SortedDictionary<uint, (uint, uint)>(); + + private readonly List<AbstractUdonBehaviourEventProxy> _eventProxies = new List<AbstractUdonBehaviourEventProxy>(); + + #endregion + + #region Editor Only + + #if UNITY_EDITOR && !VRC_CLIENT + public void RunEditorUpdate(ref bool dirty) + { + if (programSource == null) + { + return; + } + + programSource.RunEditorUpdate(this, ref dirty); + + if (!dirty) + { + return; + } + + EditorSceneManager.MarkSceneDirty(gameObject.scene); + } + + #endif + + #endregion + + #region Private Methods + + private bool LoadProgram() + { + if(serializedProgramAsset == null) + { + return false; + } + + _program = serializedProgramAsset.RetrieveProgram(); + + IUdonSymbolTable symbolTable = _program?.SymbolTable; + IUdonHeap heap = _program?.Heap; + if(symbolTable == null || heap == null) + { + return false; + } + + foreach(string variableSymbol in publicVariables.VariableSymbols) + { + if(!symbolTable.HasAddressForSymbol(variableSymbol)) + { + continue; + } + + uint symbolAddress = symbolTable.GetAddressFromSymbol(variableSymbol); + + if(!publicVariables.TryGetVariableType(variableSymbol, out Type declaredType)) + { + continue; + } + + publicVariables.TryGetVariableValue(variableSymbol, out object value); + if(declaredType == typeof(GameObject) || declaredType == typeof(UdonBehaviour) || + declaredType == typeof(Transform)) + { + if(value == null) + { + value = new UdonGameObjectComponentHeapReference(declaredType); + declaredType = typeof(UdonGameObjectComponentHeapReference); + } + } + + heap.SetHeapVariable(symbolAddress, value, declaredType); + } + + return true; + } + + private void RegisterEventProxy<T>() where T : AbstractUdonBehaviourEventProxy + { + // Exit early if we already have a match. + foreach(AbstractUdonBehaviourEventProxy existingProxy in _eventProxies) + { + if(!(existingProxy is T existingProxyAsT)) + { + continue; + } + + if(existingProxyAsT.EventReceiver.Equals(this)) + { + return; + } + } + + AbstractUdonBehaviourEventProxy proxy = gameObject.AddComponent<T>(); + #if UNITY_EDITOR + proxy.hideFlags = HideFlags.HideInInspector | + HideFlags.DontSaveInEditor | + HideFlags.DontSaveInBuild; + #endif + proxy.EventReceiver = this; + proxy.enabled = enabled; + + _eventProxies.Add(proxy); + } + + private void ProcessEntryPoints() + { + if(_program.EntryPoints.HasExportedSymbol("_interact")) + { + _hasInteractiveEvents = true; + } + + if(_program.EntryPoints.HasExportedSymbol("_update")) + { + _hasUpdateEvent = true; + } + + if(_program.EntryPoints.HasExportedSymbol("_lateUpdate")) + { + _hasLateUpdateEvent = true; + } + + if(_program.EntryPoints.HasExportedSymbol("_fixedUpdate")) + { + _hasFixedUpdateEvent = true; + } + + if(_program.EntryPoints.HasExportedSymbol("_postLateUpdate")) + { + _hasPostLateUpdateEvent = true; + } + + DetectExistingProxies(); + if(_program.EntryPoints.HasExportedSymbol("_onRenderObject")) + { + RegisterEventProxy<OnRenderObjectProxy>(); + } + + if(_program.EntryPoints.HasExportedSymbol("_onWillRenderObject")) + { + RegisterEventProxy<OnWillRenderObjectProxy>(); + } + + if(_program.EntryPoints.HasExportedSymbol("_onTriggerStay") || + _program.EntryPoints.HasExportedSymbol("_onPlayerTriggerStay")) + { + RegisterEventProxy<OnTriggerStayProxy>(); + } + + if(_program.EntryPoints.HasExportedSymbol("_onCollisionStay") || + _program.EntryPoints.HasExportedSymbol("_onPlayerCollisionStay")) + { + RegisterEventProxy<OnCollisionStayProxy>(); + } + + if(_program.EntryPoints.HasExportedSymbol("_onAnimatorMove")) + { + RegisterEventProxy<OnAnimatorMoveProxy>(); + } + + RegisterUpdate(); + + _eventTable.Clear(); + foreach(string entryPoint in _program.EntryPoints.GetExportedSymbols()) + { + uint address = _program.EntryPoints.GetAddressFromSymbol(entryPoint); + + if(!_eventTable.ContainsKey(entryPoint)) + { + _eventTable.Add(entryPoint, new List<uint>()); + } + + _eventTable[entryPoint].Add(address); + + _udonManager.RegisterInput(this, entryPoint, true); + + // check whether this is a variableChangedEvent + if(entryPoint.StartsWith(VariableChangedEvent.EVENT_PREFIX)) + { + string variableName = entryPoint.Remove(0, VariableChangedEvent.EVENT_PREFIX.Length); + // ensure the variable with the matching name exists + if(_program.SymbolTable.TryGetAddressFromSymbol(variableName, out uint variableAddress)) + { + // the old variable is only added if it's used, so just store default if it's not + _program.SymbolTable.TryGetAddressFromSymbol(string.Concat(VariableChangedEvent.OLD_VALUE_PREFIX, variableName), out uint oldVariableAddress); + + // add variable > event address lookup + _variableToChangeEvent.Add(variableAddress, (address, oldVariableAddress)); + } + } + } + } + + // GameObjects may sometimes already have proxies for this UdonBehaviour. + // For example if the GameObject was cloned from a scene GameObject, + // or the component was for some reason added in the editor (this is not an expected workflow). + // In either case Unity will serialize the reference from the proxy to this UdonBehaviour, + // but will not serialize the contents of the _eventProxies List so we need to build it. + // If we don't do this then we may create a proxy where one already exists and events will run twice. + private void DetectExistingProxies() + { + GetComponents(_eventProxies); + for(int i = _eventProxies.Count - 1; i >= 0; i--) + { + AbstractUdonBehaviourEventProxy proxy = _eventProxies[i]; + if(proxy == null) + { + _eventProxies.RemoveAt(i); + continue; + } + + UdonBehaviour proxyEventReceiver = proxy.EventReceiver; + + // Destroy and remove all copied proxy components which don't have an EventReceiver. + if(proxyEventReceiver == null) + { + Destroy(proxy); + _eventProxies.RemoveAt(i); + continue; + } + + // Remove all copied proxy components which aren't for this UdonBehaviour. + if(proxyEventReceiver == this) + { + continue; + } + + _eventProxies.RemoveAt(i); + } + } + + private bool ResolveUdonHeapReferences(IUdonSymbolTable symbolTable, IUdonHeap heap) + { + bool success = true; + foreach(string symbolName in symbolTable.GetSymbols()) + { + uint symbolAddress = symbolTable.GetAddressFromSymbol(symbolName); + object heapValue = heap.GetHeapVariable(symbolAddress); + if(!(heapValue is UdonBaseHeapReference udonBaseHeapReference)) + { + continue; + } + + if(!ResolveUdonHeapReference(heap, symbolAddress, udonBaseHeapReference)) + { + success = false; + } + } + + return success; + } + + private bool ResolveUdonHeapReference(IUdonHeap heap, uint symbolAddress, + UdonBaseHeapReference udonBaseHeapReference) + { + switch(udonBaseHeapReference) + { + case UdonGameObjectComponentHeapReference udonGameObjectComponentHeapReference: + { + Type referenceType = udonGameObjectComponentHeapReference.type; + if(referenceType == typeof(GameObject)) + { + heap.SetHeapVariable(symbolAddress, gameObject); + return true; + } + else if(referenceType == typeof(Transform)) + { + heap.SetHeapVariable(symbolAddress, gameObject.transform); + return true; + } + else if(referenceType == typeof(UdonBehaviour)) + { + heap.SetHeapVariable(symbolAddress, this); + return true; + } + else if(referenceType == typeof(Object)) + { + heap.SetHeapVariable(symbolAddress, this); + return true; + } + else + { + Logger.Log( + $"Unsupported GameObject/Component reference type: {udonBaseHeapReference.GetType().Name}. Only GameObject, Transform, and UdonBehaviour are supported.", + _debugLevel, + this); + + return false; + } + } + default: + { + Logger.Log( + $"Unknown heap reference type: {udonBaseHeapReference.GetType().Name}", + _debugLevel, + this); + + return false; + } + } + } + + #endregion + + #region Managed Unity Events + + internal void ManagedUpdate() + { + using(_managedUpdateProfilerMarker.Auto()) + { + if(!_hasDoneStart && _isReady) + { + _hasDoneStart = true; + RunEvent("_onEnable"); + RunEvent("_start"); + if(!_hasUpdateEvent) + { + _udonManager.UnregisterUdonBehaviourUpdate(this); + } + } + + RunEvent("_update"); + } + } + + internal void ManagedLateUpdate() + { + using(_managedLateUpdateProfilerMarker.Auto()) + { + RunEvent("_lateUpdate"); + } + } + + internal void ManagedFixedUpdate() + { + using(_managedFixedUpdateProfilerMarker.Auto()) + { + RunEvent("_fixedUpdate"); + } + } + + internal void PostLateUpdate() + { + using(_postLateUpdateProfilerMarker.Auto()) + { + RunEvent("_postLateUpdate"); + } + } + + #endregion + + #region Unity Events + + public void OnAnimatorIK(int layerIndex) + { + RunEvent("_onAnimatorIK", ("layerIndex", layerIndex)); + } + + internal void ProxyOnAnimatorMove() + { + RunEvent("_onAnimatorMove"); + } + + public void OnAudioFilterRead(float[] data, int channels) + { + RunEvent("_onAudioFilterRead", ("data", data), ("channels", channels)); + } + + public void OnBecameInvisible() + { + RunEvent("_onBecameInvisible"); + } + + public void OnBecameVisible() + { + RunEvent("_onBecameVisible"); + } + + public void OnCollisionEnter(Collision other) + { + var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); + if(player != null) + { + RunEvent("_onPlayerCollisionEnter", ("player", player)); + } + else if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onCollisionEnter", ("other", other)); + } + } + + public void OnCollisionEnter2D(Collision2D other) + { + if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onCollisionEnter2D", ("other", other)); + } + } + + public void OnCollisionExit(Collision other) + { + var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); + if(player != null) + { + RunEvent("_onPlayerCollisionExit", ("player", player)); + } + else if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onCollisionExit", ("other", other)); + } + } + + public void OnCollisionExit2D(Collision2D other) + { + if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onCollisionExit2D", ("other", other)); + } + } + + internal void ProxyOnCollisionStay(Collision other) + { + var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); + if(player != null) + { + RunEvent("_onPlayerCollisionStay", ("player", player)); + } + else if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onCollisionStay", ("other", other)); + } + } + + public void OnCollisionStay2D(Collision2D other) + { + if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onCollisionStay2D", ("other", other)); + } + } + + public void OnDestroy() + { + if(_program == null) + { + return; + } + + foreach(string entryPoint in _program.EntryPoints.GetExportedSymbols()) + { + _udonManager.RegisterInput(this, entryPoint, false); + } + + RunEvent("_onDestroy"); + + _udonVM = null; + _program = null; + + foreach(AbstractUdonBehaviourEventProxy proxy in _eventProxies) + { + if(proxy) + { + Destroy(proxy); + } + } + } + + public void OnDisable() + { + UnregisterUpdate(); + + RunEvent("_onDisable"); + } + + public void OnDrawGizmos() + { + RunEvent("_onDrawGizmos"); + } + + public void OnDrawGizmosSelected() + { + RunEvent("_onDrawGizmosSelected"); + } + + public void OnEnable() + { + if(_initialized) + { + RegisterUpdate(); + } + + RunEvent("_onEnable"); + } + + public void OnJointBreak(float breakForce) + { + RunEvent("_onJointBreak", ("force", breakForce)); + } + + public void OnJointBreak2D(Joint2D brokenJoint) + { + RunEvent("_onJointBreak2D", ("joint", brokenJoint)); + } + + public void OnMouseDown() + { + RunEvent("_onMouseDown"); + } + + public void OnMouseDrag() + { + RunEvent("_onMouseDrag"); + } + + public void OnMouseEnter() + { + RunEvent("_onMouseEnter"); + } + + public void OnMouseExit() + { + RunEvent("_onMouseExit"); + } + + public void OnMouseOver() + { + RunEvent("_onMouseOver"); + } + + public void OnMouseUp() + { + RunEvent("_onMouseUp"); + } + + public void OnMouseUpAsButton() + { + RunEvent("_onMouseUpAsButton"); + } + + public void OnParticleCollision(GameObject other) + { + var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); + if(player != null) + { + RunEvent("_onPlayerParticleCollision", ("player", player)); + } + else + { + RunEvent("_onParticleCollision", ("other", other)); + } + } + + public void OnParticleTrigger() + { + RunEvent("_onParticleTrigger"); + } + + public void OnPostRender() + { + RunEvent("_onPostRender"); + } + + public void OnPreCull() + { + RunEvent("_onPreCull"); + } + + public void OnPreRender() + { + RunEvent("_onPreRender"); + } + + public void OnRenderImage(RenderTexture src, RenderTexture dest) + { + if(!_eventTable.ContainsKey("_onRenderImage") || _eventTable["_onRenderImage"].Count == 0) + { + Graphics.Blit(src, dest); + return; + } + + RunEvent("_onRenderImage", ("src", src), ("dest", dest)); + } + + internal void ProxyOnRenderObject() + { + RunEvent("_onRenderObject"); + } + + public void OnTransformChildrenChanged() + { + RunEvent("_onTransformChildrenChanged"); + } + + public void OnTransformParentChanged() + { + RunEvent("_onTransformParentChanged"); + } + + public void OnTriggerEnter(Collider other) + { + var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); + if(player != null) + { + RunEvent("_onPlayerTriggerEnter", ("player", player)); + } + else if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onTriggerEnter", ("other", other)); + } + } + + public void OnTriggerEnter2D(Collider2D other) + { + if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onTriggerEnter2D", ("other", other)); + } + } + + public void OnTriggerExit(Collider other) + { + var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); + if(player != null) + { + RunEvent("_onPlayerTriggerExit", ("player", player)); + } + else if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onTriggerExit", ("other", other)); + } + } + + public void OnTriggerExit2D(Collider2D other) + { + if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onTriggerExit2D", ("other", other)); + } + } + + internal void ProxyOnTriggerStay(Collider other) + { + var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); + if(player != null) + { + RunEvent("_onPlayerTriggerStay", ("player", player)); + } + else if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onTriggerStay", ("other", other)); + } + } + + public void OnTriggerStay2D(Collider2D other) + { + if(!UdonManager.Instance.IsBlacklisted(other)) + { + RunEvent("_onTriggerStay2D", ("other", other)); + } + } + + public void OnValidate() + { + RunEvent("_onValidate"); + } + + internal void ProxyOnWillRenderObject() + { + RunEvent("_onWillRenderObject"); + } + + #endregion + + #region VRCSDK Events + + #if VRC_CLIENT + [PublicAPI] + private void OnNetworkReady() + { + _isReady = true; + } + #endif + + //Called through Interactable interface + public override void Interact() + { + RunEvent("_interact"); + } + + public override void OnDrop() + { + RunEvent("_onDrop"); + } + + public override void OnPickup() + { + RunEvent("_onPickup"); + } + + public override void OnPickupUseDown() + { + RunEvent("_onPickupUseDown"); + } + + public override void OnPickupUseUp() + { + RunEvent("_onPickupUseUp"); + } + + //Called via delegate by UdonSync + [PublicAPI] + public void OnPreSerialization() + { + if(_syncMethod == Networking.SyncType.None) + { + return; + } + + RunEvent("_onPreSerialization"); + } + + //Called via delegate by UdonSync + [PublicAPI] + public void OnPostSerialization(SerializationResult result) + { + if(_syncMethod == Networking.SyncType.None) + { + return; + } + + RunEvent("_onPostSerialization", ("result", result)); + } + + //Called via delegate by UdonSync + [PublicAPI] + public void OnDeserialization() + { + if(_syncMethod == Networking.SyncType.None) + { + return; + } + + RunEvent("_onDeserialization"); + } + + #endregion + + #region RunProgram Methods + + [PublicAPI] + public override void RunProgram(string eventName) + { + if(_program == null) + { + return; + } + + if(!_program.EntryPoints.GetExportedSymbols().Contains(eventName)) + { + return; + } + + uint address = _program.EntryPoints.GetAddressFromSymbol(eventName); + RunProgram(address); + } + + private void RunProgram(uint entryPoint) + { + if(_hasError) + { + return; + } + + if(_udonVM == null) + { + return; + } + + uint originalAddress = _udonVM.GetProgramCounter(); + UdonBehaviour originalExecuting = _udonManager.currentlyExecuting; + + _udonVM.SetProgramCounter(entryPoint); + _udonManager.currentlyExecuting = this; + + _udonVM.DebugLogging = _udonManager.DebugLogging; + + try + { + uint result = _udonVM.Interpret(); + if(result != 0) + { + Logger.LogError( + $"Udon VM execution errored, this UdonBehaviour will be halted.", + _debugLevel, + this); + + _hasError = true; + enabled = false; + } + } + catch(UdonVMException error) + { + Logger.LogError( + $"An exception occurred during Udon execution, this UdonBehaviour will be halted.\n{error}", + _debugLevel, + this); + + _hasError = true; + enabled = false; + } + + _udonManager.currentlyExecuting = originalExecuting; + if(originalAddress < 0xFFFFFFFC) + { + _udonVM.SetProgramCounter(originalAddress); + } + } + + [PublicAPI] + public ImmutableArray<string> GetPrograms() + { + return _program?.EntryPoints.GetExportedSymbols() ?? ImmutableArray<string>.Empty; + } + + #endregion + + #region Serialization + + [SerializeField] + private string serializedPublicVariablesBytesString; + + [SerializeField] + private List<Object> publicVariablesUnityEngineObjects; + + [SerializeField] + private DataFormat publicVariablesSerializationDataFormat = DataFormat.Binary; + + void ISerializationCallbackReceiver.OnAfterDeserialize() + { + DeserializePublicVariables(); + } + + private void DeserializePublicVariables() + { + byte[] serializedPublicVariablesBytes = + Convert.FromBase64String(serializedPublicVariablesBytesString ?? ""); + + publicVariables = SerializationUtility.DeserializeValue<IUdonVariableTable>( + serializedPublicVariablesBytes, + publicVariablesSerializationDataFormat, + publicVariablesUnityEngineObjects + ) ?? new UdonVariableTable(); + + // Validate that the type of the value can actually be cast to the declaredType to avoid InvalidCastExceptions later. + foreach(string publicVariableSymbol in publicVariables.VariableSymbols.ToArray()) + { + if(!publicVariables.TryGetVariableValue(publicVariableSymbol, out object value)) + { + continue; + } + + if(value == null) + { + continue; + } + + if(!publicVariables.TryGetVariableType(publicVariableSymbol, out Type declaredType)) + { + continue; + } + + if(declaredType.IsInstanceOfType(value)) + { + continue; + } + + if(declaredType.IsValueType) + { + publicVariables.TrySetVariableValue(publicVariableSymbol, Activator.CreateInstance(declaredType)); + } + else + { + publicVariables.TrySetVariableValue(publicVariableSymbol, null); + } + } + } + + void ISerializationCallbackReceiver.OnBeforeSerialize() + { + SerializePublicVariables(); + } + + private void SerializePublicVariables() + { + byte[] serializedPublicVariablesBytes = SerializationUtility.SerializeValue( + publicVariables, + publicVariablesSerializationDataFormat, + out publicVariablesUnityEngineObjects); + + serializedPublicVariablesBytesString = Convert.ToBase64String(serializedPublicVariablesBytes); + } + + #endregion + + #region IUdonBehaviour Interface + + public bool TryToInterrogateUdon(string eventName, out object returnValue, params (string symbolName, object value)[] programVariables) + { + return TryToInterrogateUdon<object>(eventName, out returnValue, programVariables); + } + + public bool TryToInterrogateUdon<T>(string eventName, out T returnValue, params (string symbolName, object value)[] programVariables) + { + if(!_initialized || !enabled || !_hasDoneStart) + { + #if VRC_CLIENT || UNITY_EDITOR + if(UdonManager.Instance.DebugLogging) + { + Logger.Log($"{gameObject.name} not ready to respond to {eventName}: initialized={_initialized} enabled={enabled} hasStarted={_hasDoneStart}", _debugLevel); + } + #endif + + returnValue = default(T); + return false; + } + + if(!_eventTable.ContainsKey(eventName)) + { + #if VRC_CLIENT || UNITY_EDITOR + if(UdonManager.Instance.DebugLogging) + { + Logger.Log($"{gameObject.name} will not respond to {eventName}", _debugLevel); + } + #endif + + returnValue = default(T); + return false; + } + + if(!RunEvent(eventName, programVariables)) + { + #if VRC_CLIENT || UNITY_EDITOR + if(UdonManager.Instance.DebugLogging) + { + Logger.LogError($"{gameObject.name} failed to respond to {eventName}", _debugLevel); + } + #endif + + returnValue = default(T); + return false; + } + + returnValue = GetProgramVariable<T>(ReturnVariableName); + return true; + } + + public override bool RunEvent(string eventName, params (string symbolName, object value)[] programVariables) + { + if(!_isReady) + { + return false; + } + + if(!_hasDoneStart) + { + return false; + } + + if(_hasError) + { + return false; + } + + if(_udonVM == null) + { + return false; + } + + if(!_eventTable.TryGetValue(eventName, out List<uint> entryPoints)) + { + return false; + } + + //TODO: Replace with a non-boxing interface before exposing to users + foreach((string symbolName, object value) in programVariables) + { + SetEventVariable(eventName, symbolName, value); + } + + foreach(uint entryPoint in entryPoints) + { + RunProgram(entryPoint); + } + + foreach((string symbolName, object _) in programVariables) + { + SetProgramVariable(symbolName, null); + } + + return true; + } + + public override void RunInputEvent(string eventName, UdonInputEventArgs args) + { + if(!_isReady) + { + return; + } + + if(!_hasDoneStart) + { + return; + } + + if(!_program.EntryPoints.GetExportedSymbols().Contains(eventName)) + { + return; + } + + // Set value arg + switch(args.eventType) + { + case UdonInputEventType.AXIS: + SetEventVariable(eventName, "floatValue", args.floatValue); + break; + case UdonInputEventType.BUTTON: + SetEventVariable(eventName, "boolValue", args.boolValue); + break; + } + + // Set event args + SetEventVariable(eventName, "args", args); + RunProgram(eventName); + } + + private void SetEventVariable<T>(string eventName, string symbolName, T value) + { + if(!_symbolNameCache.TryGetValue((eventName, symbolName), out string newSymbolName)) + { + newSymbolName = $"{eventName.Substring(1)}{char.ToUpper(symbolName.First())}{symbolName.Substring(1)}"; + _symbolNameCache.Add((eventName, symbolName), newSymbolName); + } + + SetProgramVariable(newSymbolName, value); + } + + public override void InitializeUdonContent() + { + if(_initialized) + { + return; + } + + SetupLogging(); + + _udonManager = UdonManager.Instance; + if(_udonManager == null) + { + enabled = false; + Logger.LogError( + $"Could not find the UdonManager; the UdonBehaviour on '{gameObject.name}' will not run.", + _debugLevel, + this); + + return; + } + + if(!LoadProgram()) + { + enabled = false; + Logger.Log( + $"Could not load the program; the UdonBehaviour on '{gameObject.name}' will not run.", + _debugLevel, + this); + + return; + } + + // Let UdonManager apply any processing or scans. + _udonManager.ProcessUdonProgram(_program); + + IUdonSymbolTable symbolTable = _program?.SymbolTable; + IUdonHeap heap = _program?.Heap; + if(symbolTable == null || heap == null) + { + enabled = false; + Logger.Log( + $"Invalid program; the UdonBehaviour on '{gameObject.name}' will not run.", + _debugLevel, + this); + + return; + } + + if(!ResolveUdonHeapReferences(symbolTable, heap)) + { + enabled = false; + Logger.Log( + $"Failed to resolve a GameObject/Component Reference; the UdonBehaviour on '{gameObject.name}' will not run.", + _debugLevel, + this); + + return; + } + + _udonVM = _udonManager.ConstructUdonVM(); + + if(_udonVM == null) + { + enabled = false; + Logger.LogError( + $"No UdonVM; the UdonBehaviour on '{gameObject.name}' will not run.", + _debugLevel, + this); + + return; + } + + _udonVM.LoadProgram(_program); + + ProcessEntryPoints(); + + #if !VRC_CLIENT + _isReady = true; + #else + if(!_isNetworkingSupported) + { + _isReady = true; + } + #endif + + _initialized = true; + + RunOnInit(); + } + + [PublicAPI] + public void RunOnInit() + { + if(OnInit == null) + { + return; + } + + try + { + OnInit(this, _program); + } + catch(Exception exception) + { + enabled = false; + Logger.LogError( + $"An exception '{exception.Message}' occurred during initialization; the UdonBehaviour on '{gameObject.name}' will not run. Exception:\n{exception}", + _debugLevel, + this + ); + } + } + + private void RegisterUpdate() + { + if(_udonManager == null) + { + return; + } + + if(!isActiveAndEnabled) + { + return; + } + + if(_hasUpdateEvent || !_hasDoneStart) + { + _udonManager.RegisterUdonBehaviourUpdate(this); + } + + if(_hasLateUpdateEvent) + { + _udonManager.RegisterUdonBehaviourLateUpdate(this); + } + + if(_hasFixedUpdateEvent) + { + _udonManager.RegisterUdonBehaviourFixedUpdate(this); + } + + if(_hasPostLateUpdateEvent) + { + _udonManager.RegisterUdonBehaviourPostLateUpdate(this); + } + + foreach(AbstractUdonBehaviourEventProxy proxy in _eventProxies) + { + proxy.enabled = true; + } + } + + private void UnregisterUpdate() + { + if(_udonManager == null) + { + return; + } + + if(_hasUpdateEvent) + { + _udonManager.UnregisterUdonBehaviourUpdate(this); + } + + if(_hasLateUpdateEvent) + { + _udonManager.UnregisterUdonBehaviourLateUpdate(this); + } + + if(_hasFixedUpdateEvent) + { + _udonManager.UnregisterUdonBehaviourFixedUpdate(this); + } + + if(_hasPostLateUpdateEvent) + { + _udonManager.UnregisterUdonBehaviourPostLateUpdate(this); + } + + foreach(AbstractUdonBehaviourEventProxy proxy in _eventProxies) + { + proxy.enabled = false; + } + } + + #region IUdonEventReceiver and IUdonSyncTarget Interface + + #region IUdonEventReceiver Only + + public override void SendCustomEvent(string eventName) + { + RunProgram(eventName); + } + + public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName) + { + #if UNITY_EDITOR + SendCustomEvent(eventName); + #else + SendCustomNetworkEventHook?.Invoke(this, target, eventName); + #endif + } + + public override void RequestSerialization() + { + RequestSerializationHook?.Invoke(this); + } + + public override void SendCustomEventDelayedSeconds(string eventName, float delaySeconds, EventTiming eventTiming = EventTiming.Update) + { + UdonManager.Instance.ScheduleDelayedEvent(this, eventName, delaySeconds, eventTiming); + } + + public override void SendCustomEventDelayedFrames(string eventName, int delayFrames, EventTiming eventTiming = EventTiming.Update) + { + UdonManager.Instance.ScheduleDelayedEvent(this, eventName, delayFrames, eventTiming); + } + + #endregion + + #region IUdonSyncTarget + + public override IUdonSyncMetadataTable SyncMetadataTable => _program?.SyncMetadataTable; + + #endregion + + #region Shared + + public override Type GetProgramVariableType(string symbolName) + { + if(!_program.SymbolTable.HasAddressForSymbol(symbolName)) + { + return null; + } + + uint symbolAddress = _program.SymbolTable.GetAddressFromSymbol(symbolName); + return _program.Heap.GetHeapVariableType(symbolAddress); + } + + public override void SetProgramVariable<T>(string symbolName, T value) + { + if(_program == null) + { + return; + } + + if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) + { + return; + } + + SetHeapVariable(symbolAddress, value); + } + + public override void SetProgramVariable(string symbolName, object value) + { + if(_program == null) + { + return; + } + + if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) + { + return; + } + + SetHeapVariable(symbolAddress, value); + } + + private void SetHeapVariable<T>(uint symbolAddress, T newValue) + { + if(_variableToChangeEvent.TryGetValue(symbolAddress, out (uint eventAddress, uint oldVariableAddress) data)) + { + // cache value before changing + T value = _program.Heap.GetHeapVariable<T>(symbolAddress); + + // check for change and trigger event + if(!value?.Equals(newValue) ?? newValue != null) + { + // change the variable on the heap + _program.Heap.SetHeapVariable(symbolAddress, newValue); + + // change the old variable on the heap + if(data.oldVariableAddress != uint.MaxValue) + { + _program.Heap.SetHeapVariable(data.oldVariableAddress, value); + } + + // trigger the event + RunProgram(data.eventAddress); + } + } + else + { + // just change the variable on the heap + _program.Heap.SetHeapVariable(symbolAddress, newValue); + } + } + + public override T GetProgramVariable<T>(string symbolName) + { + if(_program == null) + { + return default; + } + + if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) + { + return default; + } + + return _program.Heap.GetHeapVariable<T>(symbolAddress); + } + + public override object GetProgramVariable(string symbolName) + { + if(_program == null) + { + return null; + } + + if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) + { + #if UNITY_EDITOR + Logger.LogError($"Could not find symbol {symbolName}; available: [{string.Join(",", _program.SymbolTable.GetSymbols())}]", _debugLevel); + #endif + return null; + } + + return _program.Heap.GetHeapVariable(symbolAddress); + } + + public override bool TryGetProgramVariable<T>(string symbolName, out T value) + { + value = default; + if(_program == null) + { + return false; + } + + if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) + { + return false; + } + + return _program.Heap.TryGetHeapVariable(symbolAddress, out value); + } + + public override bool TryGetProgramVariable(string symbolName, out object value) + { + value = null; + if(_program == null) + { + return false; + } + + if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) + { + return false; + } + + return _program.Heap.TryGetHeapVariable(symbolAddress, out value); + } + + #endregion + + #endregion + + #endregion + + #region Logging Methods + + private void SetupLogging() + { + _debugLevel = GetType().GetHashCode(); + if(Logger.DebugLevelIsDescribed(_debugLevel)) + { + return; + } + + Logger.DescribeDebugLevel(_debugLevel, "UdonBehaviour"); + Logger.AddDebugLevel(_debugLevel); + } + + #endregion + + #region Manual Initialization Methods + + [PublicAPI] + public void AssignProgramAndVariables(AbstractSerializedUdonProgramAsset compiledAsset, + IUdonVariableTable variables) + { + serializedProgramAsset = compiledAsset; + publicVariables = variables; + } + + #endregion + } +} |