#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;
}
}
}
}
}