| using System; |
| using System.Collections; |
| using System.Collections.Generic; |
| using System.Linq; |
| using UnityEngine; |
|
|
| namespace Unity.VisualScripting |
| { |
| public sealed class Flow : IPoolable, IDisposable |
| { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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(); |
|
|
| |
| 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) |
| { |
| |
| 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); |
|
|
| |
| |
| 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 |
| { |
| |
| |
| |
| coroutineStopRequested = true; |
| } |
| } |
|
|
| internal void StopCoroutineImmediate() |
| { |
| if (coroutineRunner && coroutineEnumerator != null) |
| { |
| coroutineRunner.StopCoroutine(coroutineEnumerator); |
|
|
| |
| |
| ((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 |
| { |
| |
| |
| 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) |
| { |
| |
| if (!output.supportsPrediction) |
| { |
| return false; |
| } |
|
|
| var recursionNode = new RecursionNode(output, stack); |
|
|
| if (!recursion?.TryEnter(recursionNode) ?? false) |
| { |
| return false; |
| } |
|
|
| |
| 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 |
| } |
| } |
|
|