playtest-unity/playtest/Library/PackageCache/com.unity.visualscripting@1.../Runtime/VisualScripting.Flow/Flow.cs

849 lines
23 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Unity.VisualScripting
{
public sealed class Flow : IPoolable, IDisposable
{
// We need to check for recursion by passing some additional
// context information to avoid the same port in multiple different
// nested flow graphs to count as the same item. Naively,
// we're using the parent as the context, which seems to work;
// it won't theoretically catch recursive nesting, but then recursive
// nesting already isn't supported anyway, so this way we avoid hashing
// or turning the stack into a reference.
// https://support.ludiq.io/communities/5/topics/2122-r
// We make this an equatable struct to avoid any allocation.
private struct RecursionNode : IEquatable<RecursionNode>
{
public IUnitPort port { get; }
public IGraphParent context { get; }
public RecursionNode(IUnitPort port, GraphPointer pointer)
{
this.port = port;
this.context = pointer.parent;
}
public bool Equals(RecursionNode other)
{
return other.port == port && other.context == context;
}
public override bool Equals(object obj)
{
return obj is RecursionNode other && Equals(other);
}
public override int GetHashCode()
{
return HashUtility.GetHashCode(port, context);
}
}
public GraphStack stack { get; private set; }
private Recursion<RecursionNode> recursion;
private readonly Dictionary<IUnitValuePort, object> locals = new Dictionary<IUnitValuePort, object>();
public readonly VariableDeclarations variables = new VariableDeclarations();
private readonly Stack<int> loops = new Stack<int>();
private readonly HashSet<GraphStack> preservedStacks = new HashSet<GraphStack>();
public MonoBehaviour coroutineRunner { get; private set; }
private ICollection<Flow> activeCoroutinesRegistry;
private bool coroutineStopRequested;
public bool isCoroutine { get; private set; }
private IEnumerator coroutineEnumerator;
public bool isPrediction { get; private set; }
private bool disposed;
public bool enableDebug
{
get
{
if (isPrediction)
{
return false;
}
if (!stack.hasDebugData)
{
return false;
}
return true;
}
}
public static Func<GraphPointer, bool> isInspectedBinding { get; set; }
public bool isInspected => isInspectedBinding?.Invoke(stack) ?? false;
#region Lifecycle
private Flow() { }
public static Flow New(GraphReference reference)
{
Ensure.That(nameof(reference)).IsNotNull(reference);
var flow = GenericPool<Flow>.New(() => new Flow()); ;
flow.stack = reference.ToStackPooled();
return flow;
}
void IPoolable.New()
{
disposed = false;
recursion = Recursion<RecursionNode>.New();
}
public void Dispose()
{
if (disposed)
{
throw new ObjectDisposedException(ToString());
}
GenericPool<Flow>.Free(this);
}
void IPoolable.Free()
{
stack?.Dispose();
recursion?.Dispose();
locals.Clear();
loops.Clear();
variables.Clear();
// Preserved stacks could remain if coroutine was interrupted
foreach (var preservedStack in preservedStacks)
{
preservedStack.Dispose();
}
preservedStacks.Clear();
loopIdentifier = -1;
stack = null;
recursion = null;
isCoroutine = false;
coroutineEnumerator = null;
coroutineRunner = null;
activeCoroutinesRegistry?.Remove(this);
activeCoroutinesRegistry = null;
coroutineStopRequested = false;
isPrediction = false;
disposed = true;
}
public GraphStack PreserveStack()
{
var preservedStack = stack.Clone();
preservedStacks.Add(preservedStack);
return preservedStack;
}
public void RestoreStack(GraphStack stack)
{
this.stack.CopyFrom(stack);
}
public void DisposePreservedStack(GraphStack stack)
{
stack.Dispose();
preservedStacks.Remove(stack);
}
#endregion
#region Loops
public int loopIdentifier = -1;
public int currentLoop
{
get
{
if (loops.Count > 0)
{
return loops.Peek();
}
else
{
return -1;
}
}
}
public bool LoopIsNotBroken(int loop)
{
return currentLoop == loop;
}
public int EnterLoop()
{
var loop = ++loopIdentifier;
loops.Push(loop);
return loop;
}
public void BreakLoop()
{
if (currentLoop < 0)
{
throw new InvalidOperationException("No active loop to break.");
}
loops.Pop();
}
public void ExitLoop(int loop)
{
if (loop != currentLoop)
{
// Already exited through break
return;
}
loops.Pop();
}
#endregion
#region Control
public void Run(ControlOutput port)
{
Invoke(port);
Dispose();
}
public void StartCoroutine(ControlOutput port, ICollection<Flow> registry = null)
{
isCoroutine = true;
coroutineRunner = stack.component;
if (coroutineRunner == null)
{
coroutineRunner = CoroutineRunner.instance;
}
activeCoroutinesRegistry = registry;
activeCoroutinesRegistry?.Add(this);
// We have to store the enumerator because Coroutine itself
// can't be cast to IDisposable, which we'll need when stopping.
coroutineEnumerator = Coroutine(port);
coroutineRunner.StartCoroutine(coroutineEnumerator);
}
public void StopCoroutine(bool disposeInstantly)
{
if (!isCoroutine)
{
throw new NotSupportedException("Stop may only be called on coroutines.");
}
if (disposeInstantly)
{
StopCoroutineImmediate();
}
else
{
// We prefer a soft coroutine stop here that will happen at the *next frame*,
// because we don't want the flow to be disposed just yet when the event node stops
// listening, as we still need it for clean up operations.
coroutineStopRequested = true;
}
}
internal void StopCoroutineImmediate()
{
if (coroutineRunner && coroutineEnumerator != null)
{
coroutineRunner.StopCoroutine(coroutineEnumerator);
// Unity doesn't dispose coroutines enumerators when calling StopCoroutine, so we have to do it manually:
// https://forum.unity.com/threads/finally-block-not-executing-in-a-stopped-coroutine.320611/
((IDisposable)coroutineEnumerator).Dispose();
}
}
private IEnumerator Coroutine(ControlOutput startPort)
{
try
{
foreach (var instruction in InvokeCoroutine(startPort))
{
if (coroutineStopRequested)
{
yield break;
}
yield return instruction;
if (coroutineStopRequested)
{
yield break;
}
}
}
finally
{
// Manual disposal might have already occurred from StopCoroutine,
// so we have to avoid double disposal, which would throw.
if (!disposed)
{
Dispose();
}
}
}
public void Invoke(ControlOutput output)
{
Ensure.That(nameof(output)).IsNotNull(output);
var connection = output.connection;
if (connection == null)
{
return;
}
var input = connection.destination;
var recursionNode = new RecursionNode(output, stack);
BeforeInvoke(output, recursionNode);
try
{
var nextPort = InvokeDelegate(input);
if (nextPort != null)
{
Invoke(nextPort);
}
}
finally
{
AfterInvoke(output, recursionNode);
}
}
private IEnumerable InvokeCoroutine(ControlOutput output)
{
var connection = output.connection;
if (connection == null)
{
yield break;
}
var input = connection.destination;
var recursionNode = new RecursionNode(output, stack);
BeforeInvoke(output, recursionNode);
if (input.supportsCoroutine)
{
foreach (var instruction in InvokeCoroutineDelegate(input))
{
if (instruction is ControlOutput)
{
foreach (var unwrappedInstruction in InvokeCoroutine((ControlOutput)instruction))
{
yield return unwrappedInstruction;
}
}
else
{
yield return instruction;
}
}
}
else
{
ControlOutput nextPort = InvokeDelegate(input);
if (nextPort != null)
{
foreach (var instruction in InvokeCoroutine(nextPort))
{
yield return instruction;
}
}
}
AfterInvoke(output, recursionNode);
}
private RecursionNode BeforeInvoke(ControlOutput output, RecursionNode recursionNode)
{
try
{
recursion?.Enter(recursionNode);
}
catch (StackOverflowException ex)
{
output.unit.HandleException(stack, ex);
throw;
}
var connection = output.connection;
var input = connection.destination;
if (enableDebug)
{
var connectionEditorData = stack.GetElementDebugData<IUnitConnectionDebugData>(connection);
var inputUnitEditorData = stack.GetElementDebugData<IUnitDebugData>(input.unit);
connectionEditorData.lastInvokeFrame = EditorTimeBinding.frame;
connectionEditorData.lastInvokeTime = EditorTimeBinding.time;
inputUnitEditorData.lastInvokeFrame = EditorTimeBinding.frame;
inputUnitEditorData.lastInvokeTime = EditorTimeBinding.time;
}
return recursionNode;
}
private void AfterInvoke(ControlOutput output, RecursionNode recursionNode)
{
recursion?.Exit(recursionNode);
}
private ControlOutput InvokeDelegate(ControlInput input)
{
try
{
if (input.requiresCoroutine)
{
throw new InvalidOperationException($"Port '{input.key}' on '{input.unit}' can only be triggered in a coroutine.");
}
return input.action(this);
}
catch (Exception ex)
{
input.unit.HandleException(stack, ex);
throw;
}
}
private IEnumerable InvokeCoroutineDelegate(ControlInput input)
{
var instructions = input.coroutineAction(this);
while (true)
{
object instruction;
try
{
if (!instructions.MoveNext())
{
break;
}
instruction = instructions.Current;
}
catch (Exception ex)
{
input.unit.HandleException(stack, ex);
throw;
}
yield return instruction;
}
}
#endregion
#region Values
public bool IsLocal(IUnitValuePort port)
{
Ensure.That(nameof(port)).IsNotNull(port);
return locals.ContainsKey(port);
}
public void SetValue(IUnitValuePort port, object value)
{
Ensure.That(nameof(port)).IsNotNull(port);
Ensure.That(nameof(value)).IsOfType(value, port.type);
if (locals.ContainsKey(port))
{
locals[port] = value;
}
else
{
locals.Add(port, value);
}
}
public object GetValue(ValueInput input)
{
if (locals.TryGetValue(input, out var local))
{
return local;
}
var connection = input.connection;
if (connection != null)
{
if (enableDebug)
{
var connectionEditorData = stack.GetElementDebugData<IUnitConnectionDebugData>(connection);
connectionEditorData.lastInvokeFrame = EditorTimeBinding.frame;
connectionEditorData.lastInvokeTime = EditorTimeBinding.time;
}
var output = connection.source;
var value = GetValue(output);
if (enableDebug)
{
var connectionEditorData = stack.GetElementDebugData<ValueConnection.DebugData>(connection);
connectionEditorData.lastValue = value;
connectionEditorData.assignedLastValue = true;
}
return value;
}
else if (TryGetDefaultValue(input, out var defaultValue))
{
return defaultValue;
}
else
{
throw new MissingValuePortInputException(input.key);
}
}
private object GetValue(ValueOutput output)
{
if (locals.TryGetValue(output, out var local))
{
return local;
}
if (!output.supportsFetch)
{
throw new InvalidOperationException($"The value of '{output.key}' on '{output.unit}' cannot be fetched dynamically, it must be assigned.");
}
var recursionNode = new RecursionNode(output, stack);
try
{
recursion?.Enter(recursionNode);
}
catch (StackOverflowException ex)
{
output.unit.HandleException(stack, ex);
throw;
}
try
{
if (enableDebug)
{
var outputUnitEditorData = stack.GetElementDebugData<IUnitDebugData>(output.unit);
outputUnitEditorData.lastInvokeFrame = EditorTimeBinding.frame;
outputUnitEditorData.lastInvokeTime = EditorTimeBinding.time;
}
var value = GetValueDelegate(output);
return value;
}
finally
{
recursion?.Exit(recursionNode);
}
}
public object GetValue(ValueInput input, Type type)
{
return ConversionUtility.Convert(GetValue(input), type);
}
public T GetValue<T>(ValueInput input)
{
return (T)GetValue(input, typeof(T));
}
public object GetConvertedValue(ValueInput input)
{
return GetValue(input, input.type);
}
private object GetDefaultValue(ValueInput input)
{
if (!TryGetDefaultValue(input, out var defaultValue))
{
throw new InvalidOperationException("Value input port does not have a default value.");
}
return defaultValue;
}
public bool TryGetDefaultValue(ValueInput input, out object defaultValue)
{
if (!input.unit.defaultValues.TryGetValue(input.key, out defaultValue))
{
return false;
}
if (input.nullMeansSelf && defaultValue == null)
{
defaultValue = stack.self;
}
return true;
}
private object GetValueDelegate(ValueOutput output)
{
try
{
return output.getValue(this);
}
catch (Exception ex)
{
output.unit.HandleException(stack, ex);
throw;
}
}
public static object FetchValue(ValueInput input, GraphReference reference)
{
var flow = New(reference);
var result = flow.GetValue(input);
flow.Dispose();
return result;
}
public static object FetchValue(ValueInput input, Type type, GraphReference reference)
{
return ConversionUtility.Convert(FetchValue(input, reference), type);
}
public static T FetchValue<T>(ValueInput input, GraphReference reference)
{
return (T)FetchValue(input, typeof(T), reference);
}
#endregion
#region Value Prediction
public static bool CanPredict(IUnitValuePort port, GraphReference reference)
{
Ensure.That(nameof(port)).IsNotNull(port);
var flow = New(reference);
flow.isPrediction = true;
bool canPredict;
if (port is ValueInput)
{
canPredict = flow.CanPredict((ValueInput)port);
}
else if (port is ValueOutput)
{
canPredict = flow.CanPredict((ValueOutput)port);
}
else
{
throw new NotSupportedException();
}
flow.Dispose();
return canPredict;
}
private bool CanPredict(ValueInput input)
{
if (!input.hasValidConnection)
{
if (!TryGetDefaultValue(input, out var defaultValue))
{
return false;
}
if (typeof(Component).IsAssignableFrom(input.type))
{
defaultValue = defaultValue?.ConvertTo(input.type);
}
if (!input.allowsNull && defaultValue == null)
{
return false;
}
return true;
}
var output = input.validConnectedPorts.Single();
if (!CanPredict(output))
{
return false;
}
var connectedValue = GetValue(output);
if (!ConversionUtility.CanConvert(connectedValue, input.type, false))
{
return false;
}
if (typeof(Component).IsAssignableFrom(input.type))
{
connectedValue = connectedValue?.ConvertTo(input.type);
}
if (!input.allowsNull && connectedValue == null)
{
return false;
}
return true;
}
private bool CanPredict(ValueOutput output)
{
// Shortcircuit the expensive check if the port isn't marked as predictable
if (!output.supportsPrediction)
{
return false;
}
var recursionNode = new RecursionNode(output, stack);
if (!recursion?.TryEnter(recursionNode) ?? false)
{
return false;
}
// Check each value dependency
foreach (var relation in output.unit.relations.WithDestination(output))
{
if (relation.source is ValueInput)
{
var source = (ValueInput)relation.source;
if (!CanPredict(source))
{
recursion?.Exit(recursionNode);
return false;
}
}
}
var value = CanPredictDelegate(output);
recursion?.Exit(recursionNode);
return value;
}
private bool CanPredictDelegate(ValueOutput output)
{
try
{
return output.canPredictValue(this);
}
catch (Exception ex)
{
Debug.LogWarning($"Prediction check failed for '{output.key}' on '{output.unit}':\n{ex}");
return false;
}
}
public static object Predict(IUnitValuePort port, GraphReference reference)
{
Ensure.That(nameof(port)).IsNotNull(port);
var flow = New(reference);
flow.isPrediction = true;
object value;
if (port is ValueInput)
{
value = flow.GetValue((ValueInput)port);
}
else if (port is ValueOutput)
{
value = flow.GetValue((ValueOutput)port);
}
else
{
throw new NotSupportedException();
}
flow.Dispose();
return value;
}
public static object Predict(IUnitValuePort port, GraphReference reference, Type type)
{
return ConversionUtility.Convert(Predict(port, reference), type);
}
public static T Predict<T>(IUnitValuePort port, GraphReference pointer)
{
return (T)Predict(port, pointer, typeof(T));
}
#endregion
}
}