using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; using UnityEngine.SceneManagement; using UnityObject = UnityEngine.Object; namespace Unity.VisualScripting { public abstract class GraphPointer { #region Lifecycle protected static bool IsValidRoot(IGraphRoot root) { return root?.childGraph != null && root as UnityObject != null; } protected static bool IsValidRoot(UnityObject rootObject) { return rootObject != null && (rootObject as IGraphRoot)?.childGraph != null; } internal GraphPointer() { } protected void Initialize(IGraphRoot root) { if (!IsValidRoot(root)) { throw new ArgumentException("Graph pointer root must be a valid Unity object with a non-null child graph.", nameof(root)); } if (!(root is IMachine && root is MonoBehaviour || root is IMacro && root is ScriptableObject)) { throw new ArgumentException("Graph pointer root must be either a machine or a macro.", nameof(root)); } this.root = root; parentStack.Add(root); graphStack.Add(root.childGraph); dataStack.Add(machine?.graphData); debugDataStack.Add(fetchRootDebugDataBinding?.Invoke(root)); if (machine != null) { // Annoyingly, getting the gameObject property is an API call // First, we'll try using our IMachine safe reference that is assigned in play mode on Awake // If that fails, we'll try fetching it dynamically if (machine.threadSafeGameObject != null) { gameObject = machine.threadSafeGameObject; } else if (UnityThread.allowsAPI) { gameObject = component.gameObject; } else { throw new GraphPointerException("Could not fetch graph pointer root game object.", this); } } else { gameObject = null; } } protected void Initialize(IGraphRoot root, IEnumerable parentElements, bool ensureValid) { Initialize(root); Ensure.That(nameof(parentElements)).IsNotNull(parentElements); foreach (var parentElement in parentElements) { if (!TryEnterParentElement(parentElement, out var error)) { if (ensureValid) { throw new GraphPointerException(error, this); } break; } } } protected void Initialize(UnityObject rootObject, IEnumerable parentElementGuids, bool ensureValid) { Initialize(rootObject as IGraphRoot); Ensure.That(nameof(parentElementGuids)).IsNotNull(parentElementGuids); foreach (var parentElementGuid in parentElementGuids) { if (!TryEnterParentElement(parentElementGuid, out var error)) { if (ensureValid) { throw new GraphPointerException(error, this); } break; } } } #endregion #region Conversion public abstract GraphReference AsReference(); public virtual void CopyFrom(GraphPointer other) { root = other.root; gameObject = other.gameObject; parentStack.Clear(); parentElementStack.Clear(); graphStack.Clear(); dataStack.Clear(); debugDataStack.Clear(); foreach (var parent in other.parentStack) { parentStack.Add(parent); } foreach (var parentElement in other.parentElementStack) { parentElementStack.Add(parentElement); } foreach (var graph in other.graphStack) { graphStack.Add(graph); } foreach (var data in other.dataStack) { dataStack.Add(data); } foreach (var debugData in other.debugDataStack) { debugDataStack.Add(debugData); } } #endregion #region Stack public IGraphRoot root { get; protected set; } public UnityObject rootObject => root as UnityObject; public IMachine machine => root as IMachine; public IMacro macro => root as IMacro; public MonoBehaviour component => root as MonoBehaviour; public GameObject gameObject { get; private set; } public GameObject self => gameObject; public ScriptableObject scriptableObject => root as ScriptableObject; public Scene? scene { get { if (gameObject == null) { return null; } var scene = gameObject.scene; // We must allow to return unloaded scenes, because // On Enable might try fetching scene variables for example // See: https://support.ludiq.io/communities/5/topics/1864-/ if (!scene.IsValid() /* || !scene.isLoaded */) { return null; } return scene; } } public UnityObject serializedObject { get { var depth = this.depth; while (depth > 0) { var parent = parentStack[depth - 1]; if (parent.isSerializationRoot) { return parent.serializedObject; } depth--; } throw new GraphPointerException("Could not find serialized object.", this); } } protected readonly List parentStack = new List(); protected readonly List parentElementStack = new List(); protected readonly List graphStack = new List(); protected readonly List dataStack = new List(); protected readonly List debugDataStack = new List(); public IEnumerable parentElementGuids => parentElementStack.Select(parentElement => parentElement.guid); #endregion #region Utility public int depth => parentStack.Count; public bool isRoot => depth == 1; public bool isChild => depth > 1; public void EnsureDepthValid(int depth) { Ensure.That(nameof(depth)).IsGte(depth, 1); if (depth > this.depth) { throw new GraphPointerException($"Trying to fetch a graph pointer level above depth: {depth} > {this.depth}", this); } } public void EnsureChild() { if (!isChild) { throw new GraphPointerException("Graph pointer does not point to a child graph.", this); } } public bool IsWithin() where T : IGraphParent { return parent is T; } public void EnsureWithin() where T : IGraphParent { if (!IsWithin()) { throw new GraphPointerException($"Graph pointer must be within a {typeof(T)} for this operation.", this); } } public IGraphParent parent => parentStack[parentStack.Count - 1]; public T GetParent() where T : IGraphParent { EnsureWithin(); return (T)parent; } public IGraphParentElement parentElement { get { EnsureChild(); return parentElementStack[parentElementStack.Count - 1]; } } public IGraph rootGraph => graphStack[0]; public IGraph graph => graphStack[graphStack.Count - 1]; protected IGraphData _data { get => dataStack[dataStack.Count - 1]; set => dataStack[dataStack.Count - 1] = value; } public IGraphData data { get { EnsureDataAvailable(); return _data; } } protected IGraphData _parentData => dataStack[dataStack.Count - 2]; public bool hasData => _data != null; public void EnsureDataAvailable() { if (!hasData) { throw new GraphPointerException($"Graph data is not available.", this); } } public T GetGraphData() where T : IGraphData { var data = this.data; if (data is T) { return (T)data; } throw new GraphPointerException($"Graph data type mismatch. Found {data.GetType()}, expected {typeof(T)}.", this); } public T GetElementData(IGraphElementWithData element) where T : IGraphElementData { if (_data.TryGetElementData(element, out var elementData)) { if (elementData is T) { return (T)elementData; } throw new GraphPointerException($"Graph element data type mismatch. Found {elementData.GetType()}, expected {typeof(T)}.", this); } throw new GraphPointerException($"Missing graph element data for {element}.", this); } public static Func fetchRootDebugDataBinding { get; set; } public bool hasDebugData => _debugData != null; public void EnsureDebugDataAvailable() { if (!hasDebugData) { throw new GraphPointerException($"Graph debug data is not available.", this); } } protected IGraphDebugData _debugData { get => debugDataStack[debugDataStack.Count - 1]; set => debugDataStack[debugDataStack.Count - 1] = value; } public IGraphDebugData debugData { get { EnsureDebugDataAvailable(); return _debugData; } } public T GetGraphDebugData() where T : IGraphDebugData { var debugData = this.debugData; if (debugData is T) { return (T)debugData; } throw new GraphPointerException($"Graph debug data type mismatch. Found {debugData.GetType()}, expected {typeof(T)}.", this); } public T GetElementDebugData(IGraphElementWithDebugData element) { var elementDebugData = debugData.GetOrCreateElementData(element); if (elementDebugData is T) { return (T)elementDebugData; } throw new GraphPointerException($"Graph element runtime debug data type mismatch. Found {elementDebugData.GetType()}, expected {typeof(T)}.", this); } #endregion #region Traversal protected bool TryEnterParentElement(Guid parentElementGuid, out string error, int? maxRecursionDepth = null) { if (!graph.elements.TryGetValue(parentElementGuid, out var element)) { error = "Trying to enter a graph parent element with a GUID that is not within the current graph."; return false; } if (!(element is IGraphParentElement)) { error = "Provided element GUID does not point to a graph parent element."; return false; } var parentElement = (IGraphParentElement)element; return TryEnterParentElement(parentElement, out error, maxRecursionDepth); } protected bool TryEnterParentElement(IGraphParentElement parentElement, out string error, int? maxRecursionDepth = null, bool skipContainsCheck = false) { // The contains check is expensive because variant+merged collection checks // If we already know for sure this error cannot happen, skipping it provides a significant optim if (!skipContainsCheck && !graph.elements.Contains(parentElement)) { error = "Trying to enter a graph parent element that is not within the current graph."; return false; } var childGraph = parentElement.childGraph; if (childGraph == null) { error = "Trying to enter a graph parent element without a child graph."; return false; } if (Recursion.safeMode) { var recursionDepth = 0; var _maxRecursionDepth = maxRecursionDepth ?? Recursion.defaultMaxDepth; foreach (var parentGraph in graphStack) { if (parentGraph == childGraph) { recursionDepth++; } } if (recursionDepth > _maxRecursionDepth) { error = $"Max recursion depth of {_maxRecursionDepth} has been exceeded. Are you nesting a graph within itself?\nIf not, consider increasing '{nameof(Recursion)}.{nameof(Recursion.defaultMaxDepth)}'."; return false; } } EnterValidParentElement(parentElement); error = null; return true; } protected void EnterParentElement(IGraphParentElement parentElement) { if (!TryEnterParentElement(parentElement, out var error)) { throw new GraphPointerException(error, this); } } protected void EnterParentElement(Guid parentElementGuid) { if (!TryEnterParentElement(parentElementGuid, out var error)) { throw new GraphPointerException(error, this); } } private void EnterValidParentElement(IGraphParentElement parentElement) { var childGraph = parentElement.childGraph; parentStack.Add(parentElement); parentElementStack.Add(parentElement); graphStack.Add(childGraph); IGraphData childGraphData = null; _data?.TryGetChildGraphData(parentElement, out childGraphData); dataStack.Add(childGraphData); var childGraphDebugData = _debugData?.GetOrCreateChildGraphData(parentElement); debugDataStack.Add(childGraphDebugData); } protected void ExitParentElement() { if (!isChild) { throw new GraphPointerException("Trying to exit the root graph.", this); } parentStack.RemoveAt(parentStack.Count - 1); parentElementStack.RemoveAt(parentElementStack.Count - 1); graphStack.RemoveAt(graphStack.Count - 1); dataStack.RemoveAt(dataStack.Count - 1); debugDataStack.RemoveAt(debugDataStack.Count - 1); } #endregion #region Validation public bool isValid { get { try { if (rootObject == null) { // Root object has been destroyed return false; } if (rootGraph != root.childGraph) { // Root graph has changed return false; } if (serializedObject == null) { // Serialized object has been destroyed return false; } for (var depth = 1; depth < this.depth; depth++) { var parentElement = parentElementStack[depth - 1]; var parentGraph = graphStack[depth - 1]; var childGraph = graphStack[depth]; // Important to check by object and not by GUID here, // because object stack integrity has to be guaranteed // (GUID integrity is implied because they're immutable) if (!parentGraph.elements.Contains(parentElement)) { // Parent graph no longer contains the parent element return false; } if (parentElement.childGraph != childGraph) { // Child graph has changed return false; } } return true; } catch (Exception ex) { Debug.LogWarning("Failed to check graph pointer validity: \n" + ex); return false; } } } public void EnsureValid() { if (!isValid) { throw new GraphPointerException("Graph pointer is invalid.", this); } } #endregion #region Equality public bool InstanceEquals(GraphPointer other) { if (ReferenceEquals(this, other)) { return true; } if (!UnityObjectUtility.TrulyEqual(rootObject, other.rootObject)) { return false; } if (!DefinitionEquals(other)) { return false; } var depth = this.depth; // Micro optimization for (int d = 0; d < depth; d++) { var data = dataStack[d]; var otherData = other.dataStack[d]; if (data != otherData) { return false; } } return true; } public bool DefinitionEquals(GraphPointer other) { if (other == null) { return false; } if (rootGraph != other.rootGraph) { return false; } var depth = this.depth; // Micro optimization if (depth != other.depth) { return false; } for (int d = 1; d < depth; d++) { var parentElement = parentElementStack[d - 1]; var otherParentElement = other.parentElementStack[d - 1]; if (parentElement != otherParentElement) { return false; } } return true; } public int ComputeHashCode() { var hashCode = 17; hashCode = hashCode * 23 + (rootObject.AsUnityNull()?.GetHashCode() ?? 0); hashCode = hashCode * 23 + (rootGraph?.GetHashCode() ?? 0); var depth = this.depth; // Micro optimization for (int d = 1; d < depth; d++) { var parentElementGuid = parentElementStack[d - 1].guid; hashCode = hashCode * 23 + parentElementGuid.GetHashCode(); } return hashCode; } #endregion #region Breadcrumbs public override string ToString() { var sb = new StringBuilder(); sb.Append("[ "); sb.Append(rootObject.ToSafeString()); for (var depth = 1; depth < this.depth; depth++) { sb.Append(" > "); var parentElementIndex = depth - 1; if (parentElementIndex >= parentElementStack.Count) { sb.Append("?"); break; } var parentElement = parentElementStack[parentElementIndex]; sb.Append(parentElement); } sb.Append(" ]"); return sb.ToString(); } #endregion } }