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<IGraphParentElement> 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<Guid> 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<IGraphParent> parentStack = new List<IGraphParent>();
protected readonly List<IGraphParentElement> parentElementStack = new List<IGraphParentElement>();
protected readonly List<IGraph> graphStack = new List<IGraph>();
protected readonly List<IGraphData> dataStack = new List<IGraphData>();
protected readonly List<IGraphDebugData> debugDataStack = new List<IGraphDebugData>();
public IEnumerable<Guid> 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<T>() where T : IGraphParent
{
return parent is T;
}
public void EnsureWithin<T>() where T : IGraphParent
{
if (!IsWithin<T>())
{
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<T>() where T : IGraphParent
{
EnsureWithin<T>();
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<T>() 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<T>(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<IGraphRoot, IGraphDebugData> fetchRootDebugDataBinding { get; set; }
internal static Action<IGraphRoot> releaseDebugDataBinding;
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<T>() 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<T>(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
}
}