#define ENABLE_PARALLEL_PRELOAD using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading; using JetBrains.Annotations; using Unity.Profiling; using UnityEngine; using UnityEngine.SceneManagement; using VRC.Udon.ClientBindings; using VRC.Udon.ClientBindings.Interfaces; using VRC.Udon.Common; using VRC.Udon.Common.Enums; using VRC.Udon.Common.Interfaces; using VRC.Udon.Security; using VRC.Udon.Security.Interfaces; using VRC.Udon.Serialization.OdinSerializer; using VRC.Udon.Serialization.OdinSerializer.Utilities; using VRC.Udon.VM; using Logger = VRC.Core.Logger; #if VRC_CLIENT using VRC.Core; using VRC.Core.Pool; using System.Collections.Concurrent; #endif #if ENABLE_PARALLEL_PRELOAD using System.Threading.Tasks; #endif namespace VRC.Udon { [AddComponentMenu("")] public class UdonManager : MonoBehaviour, IUdonClientInterface, IUdonSecurityBlacklist, IUdonSignatureVerifier { public static event Action OnUdonProgramLoaded; public static event Action OnUdonReady; public UdonBehaviour currentlyExecuting; public bool HasLoaded { get; private set; } = false; #region Singleton private static UdonManager _instance; [PublicAPI] public static UdonManager Instance { get { #if !VRC_CLIENT if(_instance != null) { return _instance; } GameObject udonManagerGameObject = new GameObject("UdonManager"); DontDestroyOnLoad(udonManagerGameObject); _instance = udonManagerGameObject.AddComponent(); #endif return _instance; } } #endregion private static readonly UpdateOrderComparer _udonBehaviourUpdateOrderComparer = new UpdateOrderComparer(); private bool _isUdonEnabled = true; private bool _isRunningEvent; private readonly Dictionary>> _sceneUdonBehaviourDirectories = new Dictionary>>(); private readonly HashSet _udonBehavioursToRegister = new HashSet(); // depth counter is unique to each thread private ThreadLocal _udonRunProgramDepth; #region Private Update Data private readonly SortedSet _updateUdonBehaviours = new SortedSet(_udonBehaviourUpdateOrderComparer); private readonly SortedSet _lateUpdateUdonBehaviours = new SortedSet(_udonBehaviourUpdateOrderComparer); private readonly SortedSet _fixedUpdateUdonBehaviours = new SortedSet(_udonBehaviourUpdateOrderComparer); private readonly SortedSet _postLateUpdateUdonBehaviours = new SortedSet(_udonBehaviourUpdateOrderComparer); private readonly Queue<(UdonBehaviour udonBehaviour, bool newState)> _updateUdonBehavioursRegistrationQueue = new Queue<(UdonBehaviour udonBehaviour, bool newState)>(); private readonly Queue<(UdonBehaviour udonBehaviour, bool newState)> _lateUpdateUdonBehavioursRegistrationQueue = new Queue<(UdonBehaviour udonBehaviour, bool newState)>(); private readonly Queue<(UdonBehaviour udonBehaviour, bool newState)> _fixedUpdateUdonBehavioursRegistrationQueue = new Queue<(UdonBehaviour udonBehaviour, bool newState)>(); private readonly Queue<(UdonBehaviour udonBehaviour, bool newState)> _postLateUpdateUdonBehavioursRegistrationQueue = new Queue<(UdonBehaviour udonBehaviour, bool newState)>(); private PostLateUpdater _postLateUpdater; #endregion #region Private Input Data private readonly Dictionary> _inputUdonBehaviours = new Dictionary>(); private readonly Queue<(UdonBehaviour udonBehaviour, string udonEventName, bool newState)> _inputUpdateUdonBehavioursRegistrationQueue = new Queue<(UdonBehaviour udonBehaviour, string udonEventName, bool newState)>(); #endregion #region Constants [PublicAPI] public const string UDON_EVENT_ONPLAYERRESPAWN = "_onPlayerRespawn"; [PublicAPI] public const string UDON_EVENT_ONPLAYERDATAUPDATED = "_onPlayerDataUpdated"; [PublicAPI] public const string UDON_EVENT_ONPLAYERRESTORED = "_onPlayerRestored"; [PublicAPI] public const string UDON_EVENT_ONINSTANCERESTORED = "_onInstanceRestored"; [PublicAPI] public const string UDON_EVENT_ONDESERIALIZATION = "_onDeserialization"; [PublicAPI] public const string UDON_EVENT_ONSCREENUPDATE = "_onScreenUpdate"; private const int UDON_MAX_RUNPROGRAM_DEPTH = 1000; [PublicAPI] public const string UDON_EVENT_ONPOSTSERIALIZATION = "_onPostSerialization"; [PublicAPI] public const string UDON_EVENT_ONPRESERIALIZATION = "_onPreSerialization"; [PublicAPI] public const string UDON_EVENT_ONPERSISTENCEUSAGEUPDATED = "_onPersistenceUsageUpdated"; [PublicAPI] public const string UDON_EVENT_ONPLAYERDATASTORAGEEXCEEDED = "_onPlayerDataStorageExceeded"; [PublicAPI] public const string UDON_EVENT_ONPLAYERDATASTORAGEWARNING = "_onPlayerDataStorageWarning"; [PublicAPI] public const string UDON_EVENT_ONPLAYEROBJECTSTORAGEEXCEEDED = "_onPlayerObjectStorageExceeded"; [PublicAPI] public const string UDON_EVENT_ONPLAYEROBJECTSTORAGEWARNING = "_onPlayerObjectStorageWarning"; [PublicAPI] public const string UDON_EVENT_ONINSTANCESTORAGEEXCEEDED = "_onInstanceStorageExceeded"; [PublicAPI] public const string UDON_EVENT_ONINSTANCESTORAGEWARNING = "_onInstanceStorageWarning"; #region Input Actions and Axes private readonly HashSet _inputActionNames = new HashSet() { UDON_INPUT_JUMP, UDON_INPUT_USE, UDON_INPUT_GRAB, UDON_INPUT_DROP, UDON_MOVE_VERTICAL, UDON_MOVE_HORIZONTAL, UDON_LOOK_VERTICAL, UDON_LOOK_HORIZONTAL }; // Buttons [PublicAPI] public const string UDON_INPUT_JUMP = "_inputJump"; [PublicAPI] public const string UDON_INPUT_USE = "_inputUse"; [PublicAPI] public const string UDON_INPUT_GRAB = "_inputGrab"; [PublicAPI] public const string UDON_INPUT_DROP = "_inputDrop"; // Axes [PublicAPI] public const string UDON_MOVE_VERTICAL = "_inputMoveVertical"; [PublicAPI] public const string UDON_MOVE_HORIZONTAL = "_inputMoveHorizontal"; [PublicAPI] public const string UDON_LOOK_VERTICAL = "_inputLookVertical"; [PublicAPI] public const string UDON_LOOK_HORIZONTAL = "_inputLookHorizontal"; [PublicAPI] public const string UDON_EVENT_ONINPUTMETHODCHANGED = "_onInputMethodChanged"; [PublicAPI] public const string UDON_EVENT_ONLANGUAGECHANGED = "_onLanguageChanged"; #endregion [PublicAPI] public const string UDON_EVENT_ONVRCPLUSMASSGIFT = "_onVRCPlusMassGift"; #endregion private UdonTimeSource _udonTimeSource; private IUdonSecurityBlacklist _blacklist; private IUdonClientInterface _udonClientInterface; private IUdonEventScheduler _udonEventScheduler; #region SDK Only Methods #if !VRC_CLIENT [RuntimeInitializeOnLoadMethod] private static void Initialize() { UdonManager udonManager = Instance; List udonBehavioursWorkingList = new List(); int sceneCount = SceneManager.sceneCount; for(int i = 0; i < sceneCount; ++i) { Scene currentScene = SceneManager.GetSceneAt(i); if(!currentScene.isLoaded) { continue; } foreach(GameObject rootObject in currentScene.GetRootGameObjects()) { rootObject.GetComponentsInChildren(true, udonBehavioursWorkingList); foreach(UdonBehaviour udonBehaviour in udonBehavioursWorkingList) { udonManager.RegisterUdonBehaviour(udonBehaviour); } } } udonManager.HasLoaded = true; } #endif #endregion #region Signature Verification #if VRC_CLIENT private int _signatureVerificationFailed; public int SignatureVerificationFailed => _signatureVerificationFailed; private int _signatureVerificationSuccess; public int SignatureVerificationSuccess => _signatureVerificationSuccess; private int _signatureVerificationSkipped; public int SignatureVerificationSkipped => _signatureVerificationSkipped; public bool WorldSignatureVerificationEnabled { get; private set; } private VRCFastCrypto_Client.VerifyKey _signatureVerificationKey; // used as thread-safe HashSet for signature holders, since we loop behaviours but verify programs private readonly ConcurrentDictionary _verificationCache = new(); public void ResetWorldSignatureVerification() { WorldSignatureVerificationEnabled = false; _signatureVerificationKey = default; _signatureVerificationFailed = 0; _signatureVerificationSuccess = 0; _signatureVerificationSkipped = 0; _verificationCache.Clear(); } public void EnableWorldSignatureVerification(ReadOnlyMemory key) { WorldSignatureVerificationEnabled = true; _signatureVerificationKey = new VRCFastCrypto_Client.VerifyKey(key.ToArray()); } #endif #endregion #region Trigger Event Consumers private readonly List _triggerEventConsumers = new List(); public void RegisterTriggerEventConsumer(IUdonTriggerEventConsumer consumer) { if (consumer == null || _triggerEventConsumers.Contains(consumer)) return; _triggerEventConsumers.Add(consumer); _triggerEventConsumers.Sort((a, b) => a.Priority.CompareTo(b.Priority)); } public void UnregisterTriggerEventConsumer(IUdonTriggerEventConsumer consumer) { if (consumer != null) _triggerEventConsumers.Remove(consumer); } public bool TryNotifyOnTriggerEnterConsumers(UdonBehaviour udonBehaviour, Collider other) { foreach (var consumer in _triggerEventConsumers) { if (consumer.TryConsumeOnTriggerEnter(udonBehaviour, other)) return true; } return false; } public bool TryNotifyOnTriggerExitConsumers(UdonBehaviour udonBehaviour, Collider other) { foreach (var consumer in _triggerEventConsumers) { if (consumer.TryConsumeOnTriggerExit(udonBehaviour, other)) return true; } return false; } public bool TryNotifyOnTriggerStayConsumers(UdonBehaviour udonBehaviour, Collider other) { foreach (var consumer in _triggerEventConsumers) { if (consumer.TryConsumeOnTriggerStay(udonBehaviour, other)) return true; } return false; } #endregion #region Unity Event Methods public void Awake() { if(_instance == null) { _instance = this; } if(Instance != this) { if(Application.isPlaying) { Destroy(this); } else { DestroyImmediate(this); } return; } _udonTimeSource = new UdonTimeSource(); _blacklist = new UnityEngineObjectSecurityBlacklist(); _udonClientInterface = new UdonClientInterface(null,null,_blacklist); DebugLogging = Application.isEditor; _udonEventScheduler = new UdonEventScheduler(_udonTimeSource); _udonRunProgramDepth = new ThreadLocal(); _postLateUpdater = gameObject.AddComponent(); _postLateUpdater.udonManager = this; _udonEventScheduler.OnEventScheduled += HandleUdonEventScheduled; if(!Application.isPlaying) { // ReSharper disable once RedundantJumpStatement return; } #if !VRC_CLIENT PrimitiveType[] primitiveTypes = (PrimitiveType[])Enum.GetValues(typeof(PrimitiveType)); foreach(PrimitiveType primitiveType in primitiveTypes) { GameObject go = GameObject.CreatePrimitive(primitiveType); Mesh primitiveMesh = go.GetComponent().sharedMesh; Destroy(go); Blacklist(primitiveMesh); } #endif } #if !VRC_CLIENT // pi: in client CustomScenes calls OnSceneLoaded with the right timing private void OnEnable() { SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; } private void OnDisable() { SceneManager.sceneLoaded -= OnSceneLoaded; SceneManager.sceneUnloaded -= OnSceneUnloaded; } #endif private void Update() { _udonTimeSource.UpdateTime(Time.deltaTime); bool anyNull = false; foreach(UdonBehaviour udonBehaviour in _updateUdonBehaviours) { if(udonBehaviour == null) { anyNull = true; continue; } udonBehaviour.ManagedUpdate(); } while(_updateUdonBehavioursRegistrationQueue.Count > 0) { (UdonBehaviour udonBehaviour, bool newState) = _updateUdonBehavioursRegistrationQueue.Dequeue(); if(newState) { _updateUdonBehaviours.Add(udonBehaviour); } else { _updateUdonBehaviours.Remove(udonBehaviour); } } if(anyNull) { _updateUdonBehaviours.RemoveWhere(o => o == null); } UpdateInputQueue(); _udonEventScheduler.RunScheduledEvents(EventTiming.Update); } private void LateUpdate() { bool anyNull = false; foreach(UdonBehaviour udonBehaviour in _lateUpdateUdonBehaviours) { if(udonBehaviour == null) { anyNull = true; continue; } udonBehaviour.ManagedLateUpdate(); } while(_lateUpdateUdonBehavioursRegistrationQueue.Count > 0) { (UdonBehaviour udonBehaviour, bool newState) = _lateUpdateUdonBehavioursRegistrationQueue.Dequeue(); if(newState) { _lateUpdateUdonBehaviours.Add(udonBehaviour); } else { _lateUpdateUdonBehaviours.Remove(udonBehaviour); } } if(anyNull) { _lateUpdateUdonBehaviours.RemoveWhere(o => o == null); } _udonEventScheduler.RunScheduledEvents(EventTiming.LateUpdate); } private void FixedUpdate() { bool anyNull = false; foreach(UdonBehaviour udonBehaviour in _fixedUpdateUdonBehaviours) { if(udonBehaviour == null) { anyNull = true; continue; } udonBehaviour.ManagedFixedUpdate(); } while(_fixedUpdateUdonBehavioursRegistrationQueue.Count > 0) { (UdonBehaviour udonBehaviour, bool newState) = _fixedUpdateUdonBehavioursRegistrationQueue.Dequeue(); if(newState) { _fixedUpdateUdonBehaviours.Add(udonBehaviour); } else { _fixedUpdateUdonBehaviours.Remove(udonBehaviour); } } if(anyNull) { _fixedUpdateUdonBehaviours.RemoveWhere(o => o == null); } _udonEventScheduler.RunScheduledEvents(EventTiming.FixedUpdate); } internal void PostLateUpdate() { bool anyNull = false; foreach(UdonBehaviour udonBehaviour in _postLateUpdateUdonBehaviours) { if(udonBehaviour == null) { anyNull = true; continue; } udonBehaviour.PostLateUpdate(); } while(_postLateUpdateUdonBehavioursRegistrationQueue.Count > 0) { (UdonBehaviour udonBehaviour, bool newState) = _postLateUpdateUdonBehavioursRegistrationQueue.Dequeue(); if(newState) { _postLateUpdateUdonBehaviours.Add(udonBehaviour); } else { _postLateUpdateUdonBehaviours.Remove(udonBehaviour); } } if(anyNull) { _postLateUpdateUdonBehaviours.RemoveWhere(o => o == null); } _postLateUpdater.enabled = _postLateUpdateUdonBehaviours.Count != 0 || _udonEventScheduler.HasAnyPendingScheduledEvents(EventTiming.PostLateUpdate); _udonEventScheduler.RunScheduledEvents(EventTiming.PostLateUpdate); } private void OnDestroy() { if (_udonEventScheduler != null) { _udonEventScheduler.OnEventScheduled -= HandleUdonEventScheduled; } _udonRunProgramDepth?.Dispose(); _udonRunProgramDepth = null; } #endregion #region Input Methods public T GetWrapperModule(string wrapperModuleName) where T : IUdonWrapperModule { try { return (T)_udonClientInterface.GetWrapper().GetWrapperModuleByName(wrapperModuleName); } catch (Exception e) { UnityEngine.Debug.LogException(e); } return default(T); } [PublicAPI] public void RegisterInput(UdonBehaviour udonBehaviour, string udonEventName, bool doRegister) { _inputUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, udonEventName, doRegister)); } [PublicAPI] public void RunInputAction(string inputEvent, UdonInputEventArgs args) { if(!_inputUdonBehaviours.TryGetValue(inputEvent, out HashSet udonBehaviours)) { return; } foreach(UdonBehaviour udonBehaviour in udonBehaviours) { // need to use this equals style, just checking if(udonBehaviour) does not return correctly if(udonBehaviour == null) { _inputUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, inputEvent, false)); continue; } // Easier to check here than adding / removing from lookup if(udonBehaviour.enabled) { udonBehaviour.RunInputEvent(inputEvent, args); } } } private void UpdateInputQueue() { while(_inputUpdateUdonBehavioursRegistrationQueue.Count > 0) { // Get next item in queue (UdonBehaviour udonBehaviour, string eventName, bool newState) = _inputUpdateUdonBehavioursRegistrationQueue.Dequeue(); // Skip if this is not an input event if(!(_inputActionNames.Contains(eventName))) { continue; } // Needs to be added to lookup if(newState) { // Add to existing set if(_inputUdonBehaviours.TryGetValue(eventName, out HashSet udonBehaviours)) { udonBehaviours.Add(udonBehaviour); } // Or create new one with this UdonBehaviour in it else { _inputUdonBehaviours.Add(eventName, new HashSet() { udonBehaviour }); } } // Needs to be removed from lookup else { if(_inputUdonBehaviours.TryGetValue(eventName, out HashSet udonBehaviours)) { udonBehaviours.Remove(udonBehaviour); } } } } #endregion #region Scene Load Methods private ProfilerMarker _preloadProfilerMarker = new ProfilerMarker("UdonManager.OnSceneLoaded Preload"); private ProfilerMarker _initializeProfilerMarker = new ProfilerMarker("UdonManager.OnSceneLoaded Initialize"); public bool IsSceneLoading { get; private set; } = false; public void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode) { Stopwatch timer = new Stopwatch(); timer.Start(); IsSceneLoading = true; try { if(loadSceneMode == LoadSceneMode.Single) { _sceneUdonBehaviourDirectories.Clear(); } Dictionary> sceneUdonBehaviourDirectory = new Dictionary>(); #if !VRC_CLIENT List transformsTempList = new List(); #endif foreach(GameObject rootGameObject in scene.GetRootGameObjects()) { #if VRC_CLIENT using (rootGameObject.GetComponentsInChildrenPooled(out List transformsTempList, true)) #else rootGameObject.GetComponentsInChildren(true, transformsTempList); #endif { foreach(Transform currentTransform in transformsTempList) { GameObject currentGameObject = currentTransform.gameObject; #if VRC_CLIENT using (currentGameObject.GetComponentsPooled(out List currentGameObjectUdonBehaviours)) #else List currentGameObjectUdonBehaviours = new List(); currentGameObject.GetComponents(currentGameObjectUdonBehaviours); #endif { if(currentGameObjectUdonBehaviours.Count > 0) { sceneUdonBehaviourDirectory.Add( currentGameObject, new HashSet(currentGameObjectUdonBehaviours)); } } } } } if(!_isUdonEnabled) { Logger.LogWarning( "Udon is disabled globally, Udon components will be removed from the scene."); foreach(HashSet udonBehaviours in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviours) { Destroy(udonBehaviour); } } return; } _sceneUdonBehaviourDirectories.Add(scene, sceneUdonBehaviourDirectory); // Initialize Event Queues - we don't want any cached UdonBehaviours or Events from previous scenes _updateUdonBehaviours.Clear(); _lateUpdateUdonBehaviours.Clear(); _fixedUpdateUdonBehaviours.Clear(); _postLateUpdateUdonBehaviours.Clear(); _postLateUpdater.enabled = false; _updateUdonBehavioursRegistrationQueue.Clear(); _lateUpdateUdonBehavioursRegistrationQueue.Clear(); _fixedUpdateUdonBehavioursRegistrationQueue.Clear(); _postLateUpdateUdonBehavioursRegistrationQueue.Clear(); _inputUdonBehaviours.Clear(); _inputUpdateUdonBehavioursRegistrationQueue.Clear(); _udonEventScheduler.ClearScheduledEvents(); Texture2DDefaultTextureHolder.ResetTextures(); #if ENABLE_PARALLEL_PRELOAD Parallel.ForEach( sceneUdonBehaviourDirectory.Values, new ParallelOptions { MaxDegreeOfParallelism = 3 }, udonBehaviourList => { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { using(_preloadProfilerMarker.Auto()) { udonBehaviour.PreloadUdonProgram(this); } } }); #else foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { using(_preloadProfilerMarker.Auto()) { udonBehaviour.PreloadUdonProgram(this); } } } #endif // Initialize all UdonBehaviours in the scene so their Public Variables are populated. foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { // All UdonBehaviours that exist in the scene get networking setup automatically. udonBehaviour.IsNetworkingSupported = true; using(_initializeProfilerMarker.Auto()) { udonBehaviour.InitializeUdonContent(); } } } } finally { PurgeSerializationCaches(); Logger.Log($"UdonManager.OnSceneLoaded took '{timer.Elapsed.TotalSeconds:N3}'"); OnUdonReady?.Invoke(); IsSceneLoading = false; } } bool IUdonSignatureVerifier.VerifySignature(IUdonSignatureHolder signatureHolder) { #if VRC_CLIENT if (WorldSignatureVerificationEnabled && signatureHolder is { IsInternallyValidated: false } && _verificationCache.TryAdd(signatureHolder, 0)) { var data = signatureHolder.SignedData; var signature = signatureHolder.Signature; var result = VRCFastCrypto_Client.VerifyMessage(_signatureVerificationKey, data, signature); if (result is VRCFastCrypto_Client.LibResult.Success) { Interlocked.Increment(ref _signatureVerificationSuccess); return true; } Interlocked.Increment(ref _signatureVerificationFailed); return false; } Interlocked.Increment(ref _signatureVerificationSkipped); return true; #else return true; #endif } [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] public void ProcessUdonProgram(IUdonProgram udonProgram) { OnUdonProgramLoaded?.Invoke(udonProgram); } private void OnSceneUnloaded(Scene scene) { PurgeSerializationCaches(); _sceneUdonBehaviourDirectories.Remove(scene); } private void PurgeSerializationCaches() { Cache.Purge(); Cache.Purge(); Cache.Purge(); Cache.Purge(); Cache.Purge(); } public ulong GetTotalLoadedProgramSize() { ulong totalSize = 0L; #if VRC_CLIENT using (HashSetPool.Get(out HashSet observedProgramIds)) #else HashSet observedProgramIds = new HashSet(); #endif { foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { if(udonBehaviour != null) { int programId = udonBehaviour.ProgramId; if (programId != 0 && !observedProgramIds.Contains(programId)) { totalSize += udonBehaviour.ProgramSize; observedProgramIds.Add(udonBehaviour.ProgramId); } } } } } } return totalSize; } public void GetLoadedBehavioursSyncTypes(out int syncNone, out int syncManual, out int syncContinuous, out int syncUnknown) { syncNone = syncManual = syncContinuous = syncUnknown = 0; foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { switch (udonBehaviour.SyncMethod) { case SDKBase.Networking.SyncType.None: syncNone++; break; case SDKBase.Networking.SyncType.Manual: syncManual++; break; case SDKBase.Networking.SyncType.Continuous: syncContinuous++; break; default: syncUnknown++; break; } } } } } #endregion #region Update Registration Methods internal void RegisterUdonBehaviourUpdate(UdonBehaviour udonBehaviour) => _updateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, true)); internal void RegisterUdonBehaviourLateUpdate(UdonBehaviour udonBehaviour) => _lateUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, true)); internal void RegisterUdonBehaviourFixedUpdate(UdonBehaviour udonBehaviour) => _fixedUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, true)); internal void RegisterUdonBehaviourPostLateUpdate(UdonBehaviour udonBehaviour) { _postLateUpdater.enabled = true; _postLateUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, true)); } internal void UnregisterUdonBehaviourUpdate(UdonBehaviour udonBehaviour) => _updateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, false)); internal void UnregisterUdonBehaviourLateUpdate(UdonBehaviour udonBehaviour) => _lateUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, false)); internal void UnregisterUdonBehaviourFixedUpdate(UdonBehaviour udonBehaviour) => _fixedUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, false)); internal void UnregisterUdonBehaviourPostLateUpdate(UdonBehaviour udonBehaviour) => _postLateUpdateUdonBehavioursRegistrationQueue.Enqueue((udonBehaviour, false)); #endregion #region Event Scheduler Methods [PublicAPI] public void ScheduleDelayedEvent(IUdonEventReceiver eventReceiver, string eventName, float delaySeconds, EventTiming eventTiming) => _udonEventScheduler.ScheduleDelayedSecondsEvent(eventReceiver, eventName, delaySeconds, eventTiming); [PublicAPI] public void ScheduleDelayedEvent(IUdonEventReceiver eventReceiver, string eventName, int delayFrames, EventTiming eventTiming) => _udonEventScheduler.ScheduleDelayedFramesEvent(eventReceiver, eventName, delayFrames, eventTiming); private void HandleUdonEventScheduled(EventTiming eventTiming) { if (eventTiming == EventTiming.PostLateUpdate) { _postLateUpdater.enabled = true; } } #endregion #region Control Methods [PublicAPI] public void SetUdonEnabled(bool isEnabled) { _isUdonEnabled = isEnabled; } public void IncrementDepthCount() { // avoid accessing disposed or null value if (_udonRunProgramDepth != null) { _udonRunProgramDepth.Value++; if (_udonRunProgramDepth.Value == UDON_MAX_RUNPROGRAM_DEPTH) { _udonRunProgramDepth.Value = 0; throw new UdonVMException( $"Stack overflow detected. Recursion depth of UdonBehaviour exceeded the limit."); } } } public void DecrementDepthCount() { // avoid accessing disposed or null value if (_udonRunProgramDepth != null) { _udonRunProgramDepth.Value--; if (_udonRunProgramDepth.Value < 0) { _udonRunProgramDepth.Value = 0; } } } #endregion #region IUdonClientInterface Methods public bool DebugLogging { get => _udonClientInterface.DebugLogging; set => _udonClientInterface.DebugLogging = value; } public IUdonVM ConstructUdonVM() { return !_isUdonEnabled ? null : _udonClientInterface.ConstructUdonVM(); } public void ApplyFilter(ref T objectToFilter) where T : class { _udonClientInterface.ApplyFilter(ref objectToFilter); } public void Blacklist(UnityEngine.Object objectToBlacklist, bool includeParents = true) { _blacklist.Blacklist(objectToBlacklist, includeParents); } public void Blacklist(IEnumerable objectsToBlacklist, bool includeParents = true) { _blacklist.Blacklist(objectsToBlacklist, includeParents); } public void ApplyFilter(ref UnityEngine.Object objectToFilter) { _udonClientInterface.ApplyFilter(ref objectToFilter); } public void CleanBlacklist() { _blacklist.CleanBlacklist(); } public bool IsBlacklisted(UnityEngine.Object objectToCheck) { return _blacklist.IsBlacklisted(objectToCheck); } public bool IsBlacklisted(T objectToCheck) { return _blacklist.IsBlacklisted(objectToCheck); } public void ApplyLightCullingMaskFilter(ref int lightCullingMask) { _udonClientInterface.ApplyLightCullingMaskFilter(ref lightCullingMask); } public int LightReservedLayerMask { get => _blacklist.LightReservedLayerMask; set => _blacklist.LightReservedLayerMask = value; } public IUdonWrapper GetWrapper() { return _udonClientInterface.GetWrapper(); } #endregion [PublicAPI] public void RegisterUdonBehaviour(UdonBehaviour udonBehaviour) { // if an event is currently being processed, wait till after to add it to our dictionaries // we still need to call InitializeUdonContent though, so code in the event executing can access the behaviour immediately // InitializeUdonContent can be called multiple times with no issue if (_isRunningEvent) { udonBehaviour.InitializeUdonContent(); _udonBehavioursToRegister.Add(udonBehaviour); return; } GameObject udonBehaviourGameObject = udonBehaviour.gameObject; Scene udonBehaviourScene = udonBehaviourGameObject.scene; if(!_sceneUdonBehaviourDirectories.TryGetValue( udonBehaviourScene, out Dictionary> sceneUdonBehaviourDirectory)) { sceneUdonBehaviourDirectory = new Dictionary>(); _sceneUdonBehaviourDirectories.Add(udonBehaviourScene, sceneUdonBehaviourDirectory); } if(sceneUdonBehaviourDirectory.TryGetValue( udonBehaviourGameObject, out HashSet gameObjectUdonBehaviours)) { gameObjectUdonBehaviours.Add(udonBehaviour); } else { gameObjectUdonBehaviours = new HashSet { udonBehaviour }; sceneUdonBehaviourDirectory.Add(udonBehaviourGameObject, gameObjectUdonBehaviours); } udonBehaviour.InitializeUdonContent(); } [PublicAPI] public void UnregisterUdonBehaviour(UdonBehaviour udonBehaviour) { GameObject udonBehaviourGameObject = udonBehaviour.gameObject; Scene udonBehaviourScene = udonBehaviourGameObject.scene; if(!_sceneUdonBehaviourDirectories.TryGetValue( udonBehaviourScene, out Dictionary> sceneUdonBehaviourDirectory)) { return; } sceneUdonBehaviourDirectory.Remove(udonBehaviourGameObject); _udonBehavioursToRegister.Remove(udonBehaviour); } private void CheckUdonBehavioursToRegister() { if (_udonBehavioursToRegister.Count > 0) { _udonBehavioursToRegister.ForEach(RegisterUdonBehaviour); _udonBehavioursToRegister.Clear(); } } public List UdonBehavioursInScene = new List(); [PublicAPI] public List GetUdonBehavioursInScene() { UdonBehavioursInScene.Clear(); foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { if(udonBehaviour != null) { UdonBehavioursInScene.Add(udonBehaviour); } } } } return UdonBehavioursInScene; } #region Global RunEvent Methods //Run an udon event on all objects [PublicAPI] public void RunEvent(string eventName) { _isRunningEvent = true; foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { if(udonBehaviour != null) { udonBehaviour.RunEvent(eventName); } } } } _isRunningEvent = false; CheckUdonBehavioursToRegister(); } [PublicAPI] public void RunEvent(string eventName, (string symbolName, T0 value) parameter0) { _isRunningEvent = true; foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { if(udonBehaviour != null) { udonBehaviour.RunEvent(eventName, parameter0); } } } } _isRunningEvent = false; CheckUdonBehavioursToRegister(); } [PublicAPI] public void RunEvent(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1) { _isRunningEvent = true; foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { if(udonBehaviour != null) { udonBehaviour.RunEvent(eventName, parameter0, parameter1); } } } } _isRunningEvent = false; CheckUdonBehavioursToRegister(); } [PublicAPI] public void RunEvent(string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2) { _isRunningEvent = true; foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { if(udonBehaviour != null) { udonBehaviour.RunEvent(eventName, parameter0, parameter1, parameter2); } } } } _isRunningEvent = false; CheckUdonBehavioursToRegister(); } [PublicAPI] public void RunEvent(string eventName, params (string symbolName, object value)[] eventParameters) { _isRunningEvent = true; foreach(Dictionary> sceneUdonBehaviourDirectory in _sceneUdonBehaviourDirectories.Values) { foreach(HashSet udonBehaviourList in sceneUdonBehaviourDirectory.Values) { foreach(UdonBehaviour udonBehaviour in udonBehaviourList) { if(udonBehaviour != null) { udonBehaviour.RunEvent(eventName, eventParameters); } } } } _isRunningEvent = false; CheckUdonBehavioursToRegister(); } //Run an udon event on a specific gameObject [PublicAPI] public void RunEvent(GameObject eventReceiverObject, string eventName) { if(!_sceneUdonBehaviourDirectories.TryGetValue( eventReceiverObject.scene, out Dictionary> sceneUdonBehaviourDirectory)) { return; } if(!sceneUdonBehaviourDirectory.TryGetValue( eventReceiverObject, out HashSet eventReceiverBehaviourList)) { return; } foreach(UdonBehaviour udonBehaviour in eventReceiverBehaviourList) { udonBehaviour.RunEvent(eventName); } } [PublicAPI] public void RunEvent(GameObject eventReceiverObject, string eventName, (string symbolName, T0 value) parameter0) { if(!_sceneUdonBehaviourDirectories.TryGetValue( eventReceiverObject.scene, out Dictionary> sceneUdonBehaviourDirectory)) { return; } if(!sceneUdonBehaviourDirectory.TryGetValue( eventReceiverObject, out HashSet eventReceiverBehaviourList)) { return; } foreach(UdonBehaviour udonBehaviour in eventReceiverBehaviourList) { udonBehaviour.RunEvent(eventName, parameter0); } } [PublicAPI] public void RunEvent(GameObject eventReceiverObject, string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1) { if(!_sceneUdonBehaviourDirectories.TryGetValue( eventReceiverObject.scene, out Dictionary> sceneUdonBehaviourDirectory)) { return; } if(!sceneUdonBehaviourDirectory.TryGetValue( eventReceiverObject, out HashSet eventReceiverBehaviourList)) { return; } foreach(UdonBehaviour udonBehaviour in eventReceiverBehaviourList) { udonBehaviour.RunEvent(eventName, parameter0, parameter1); } } [PublicAPI] public void RunEvent(GameObject eventReceiverObject, string eventName, (string symbolName, T0 value) parameter0, (string symbolName, T1 value) parameter1, (string symbolName, T2 value) parameter2) { if(!_sceneUdonBehaviourDirectories.TryGetValue( eventReceiverObject.scene, out Dictionary> sceneUdonBehaviourDirectory)) { return; } if(!sceneUdonBehaviourDirectory.TryGetValue( eventReceiverObject, out HashSet eventReceiverBehaviourList)) { return; } foreach(UdonBehaviour udonBehaviour in eventReceiverBehaviourList) { udonBehaviour.RunEvent(eventName, parameter0, parameter1, parameter2); } } [PublicAPI] public void RunEvent(GameObject eventReceiverObject, string eventName, params (string symbolName, object value)[] eventParameters) { if(!_sceneUdonBehaviourDirectories.TryGetValue( eventReceiverObject.scene, out Dictionary> sceneUdonBehaviourDirectory)) { return; } if(!sceneUdonBehaviourDirectory.TryGetValue( eventReceiverObject, out HashSet eventReceiverBehaviourList)) { return; } foreach(UdonBehaviour udonBehaviour in eventReceiverBehaviourList) { udonBehaviour.RunEvent(eventName, eventParameters); } } #endregion #region Helper Classes private class UpdateOrderComparer : IComparer { public int Compare(UdonBehaviour x, UdonBehaviour y) { if(x == null) { return y != null ? -1 : 0; } if(y == null) { return 1; } int updateOrderComparison = x.UpdateOrder.CompareTo(y.UpdateOrder); if(updateOrderComparison != 0) { return updateOrderComparison; } return x.GetInstanceID().CompareTo(y.GetInstanceID()); } } private class UdonTimeSource : IUdonEventSchedulerTimeSource { public double CurrentTime { get; private set; } = 0; public long CurrentFrame { get; private set; } = 0; public float MinimumDelay => 0.001f; public void UpdateTime(float deltaTime) { CurrentTime += deltaTime; CurrentFrame++; } } #endregion } }