#nullable enable using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using JetBrains.Annotations; using nadena.dev.ndmf.runtime.components; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; #if NDMF_VRCSDK3_AVATARS using VRC.SDK3.Avatars.Components; #endif namespace nadena.dev.ndmf.runtime { /// /// A collection of general-purpose utilities that are available from Runtime-scope scripts. /// public static class RuntimeUtil { internal static HashSet AllRootTypes = new HashSet() { typeof(NDMFAvatarRoot), }; /// /// Invoke this function to register a callback with EditorApplication.delayCall from a context that cannot /// access EditorApplication. /// public static Action DelayCall { get; internal set; } static RuntimeUtil() { DelayCall = action => { throw new Exception("delayCall() cannot be called during static initialization"); }; } // Shadow the VRC-provided methods to avoid deprecation warnings internal static T GetOrAddComponent(this GameObject obj) where T : Component { if (!obj.TryGetComponent(out var component)) component = obj.AddComponent(); return component; } internal static T GetOrAddComponent(this Component obj) where T : Component { return obj.gameObject.GetOrAddComponent(); } /// /// Returns whether the editor is in play mode. /// #if UNITY_EDITOR public static bool IsPlaying => EditorApplication.isPlayingOrWillChangePlaymode; #else public static bool IsPlaying => true; #endif /// /// Returns the relative path from root to child, or null is child is not a descendant of root. /// /// /// /// public static string? RelativePath(GameObject? root, GameObject? child) { return RelativePath(root?.transform, child?.transform); } /// /// Returns the relative path from root to child, or null is child is not a descendant of root. /// /// /// /// public static string? RelativePath(Transform? root, Transform? child) { if (root == child) return ""; var pathSegments = new List(); while (child != root && child != null) { pathSegments.Add(child.gameObject.name); child = child.parent; } if (child == null && root != null) return null; pathSegments.Reverse(); return string.Join("/", pathSegments); } private static Component? GetAvatarRootInThisAndParents(Transform? t) { Component? candidate = null; while (t != null) { foreach (var ty in AllRootTypes) { if (t.TryGetComponent(ty, out Component c)) { candidate = c; } } t = t.parent; } return candidate; } /// /// Returns the path of a game object relative to the avatar root, or null if the avatar root could not be /// located. /// /// /// public static string? AvatarRootPath(GameObject child) { if (child == null) return null; var avatar = FindAvatarInParents(child.transform); if (avatar == null) return null; return RelativePath(avatar.gameObject, child); } /// /// Check whether the target component is the root of the avatar. /// /// /// public static bool IsAvatarRoot(Transform target) { return (AllRootTypes.Any(ty => target.TryGetComponent(ty, out _))) && (GetAvatarRootInThisAndParents(target.parent) == null); } /// /// Return a list of avatar roots in the current Scene(s). This function is a heuristic, and the details /// of its operation may change in patch releases. /// /// public static IEnumerable FindAvatarRoots(GameObject? root = null) { if (root == null) { var sceneCount = SceneManager.sceneCount; for (int i = 0; i < sceneCount; i++) { var scene = SceneManager.GetSceneAt(i); if (!scene.isLoaded) continue; foreach (var avatar in FindAvatarsInScene(scene)) { yield return avatar.gameObject; } } } else { GameObject? priorRoot = null; OrderedDictionary candidates = new(); foreach (var ty in AllRootTypes) { foreach (var c in root.GetComponentsInChildren(ty, false)) { candidates[c] = true; } } foreach (var candidate in candidates.Keys.OfType()) { var gameObject = candidate.gameObject; // Ignore nested candidates if (GetAvatarRootInThisAndParents(gameObject.transform.parent) != null) continue; priorRoot = gameObject; yield return candidate.gameObject; } } } /// /// Returns the component marking the root of the avatar. /// /// /// public static Transform? FindAvatarInParents(Transform? target) { return GetAvatarRootInThisAndParents(target)?.transform; } /// /// Returns the component marking the root of the avatar. /// /// /// internal static IEnumerable FindAvatarsInScene(Scene scene) { foreach (var root in scene.GetRootGameObjects()) { foreach (var avatar in FindAvatarRoots(root)) { yield return avatar.transform; } } } } }