A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Linq;
5using UnityEngine;
6
7namespace Unity.VisualScripting
8{
9 public sealed class Flow : IPoolable, IDisposable
10 {
11 // We need to check for recursion by passing some additional
12 // context information to avoid the same port in multiple different
13 // nested flow graphs to count as the same item. Naively,
14 // we're using the parent as the context, which seems to work;
15 // it won't theoretically catch recursive nesting, but then recursive
16 // nesting already isn't supported anyway, so this way we avoid hashing
17 // or turning the stack into a reference.
18 // https://support.ludiq.io/communities/5/topics/2122-r
19 // We make this an equatable struct to avoid any allocation.
20 private struct RecursionNode : IEquatable<RecursionNode>
21 {
22 public IUnitPort port { get; }
23
24 public IGraphParent context { get; }
25
26 public RecursionNode(IUnitPort port, GraphPointer pointer)
27 {
28 this.port = port;
29 this.context = pointer.parent;
30 }
31
32 public bool Equals(RecursionNode other)
33 {
34 return other.port == port && other.context == context;
35 }
36
37 public override bool Equals(object obj)
38 {
39 return obj is RecursionNode other && Equals(other);
40 }
41
42 public override int GetHashCode()
43 {
44 return HashUtility.GetHashCode(port, context);
45 }
46 }
47
48 public GraphStack stack { get; private set; }
49
50 private Recursion<RecursionNode> recursion;
51
52 private readonly Dictionary<IUnitValuePort, object> locals = new Dictionary<IUnitValuePort, object>();
53
54 public readonly VariableDeclarations variables = new VariableDeclarations();
55
56 private readonly Stack<int> loops = new Stack<int>();
57
58 private readonly HashSet<GraphStack> preservedStacks = new HashSet<GraphStack>();
59
60 public MonoBehaviour coroutineRunner { get; private set; }
61
62 private ICollection<Flow> activeCoroutinesRegistry;
63
64 private bool coroutineStopRequested;
65
66 public bool isCoroutine { get; private set; }
67
68 private IEnumerator coroutineEnumerator;
69
70 public bool isPrediction { get; private set; }
71
72 private bool disposed;
73
74 public bool enableDebug
75 {
76 get
77 {
78 if (isPrediction)
79 {
80 return false;
81 }
82
83 if (!stack.hasDebugData)
84 {
85 return false;
86 }
87
88 return true;
89 }
90 }
91
92 public static Func<GraphPointer, bool> isInspectedBinding { get; set; }
93
94 public bool isInspected => isInspectedBinding?.Invoke(stack) ?? false;
95
96
97 #region Lifecycle
98
99 private Flow() { }
100
101 public static Flow New(GraphReference reference)
102 {
103 Ensure.That(nameof(reference)).IsNotNull(reference);
104
105 var flow = GenericPool<Flow>.New(() => new Flow()); ;
106 flow.stack = reference.ToStackPooled();
107
108 return flow;
109 }
110
111 void IPoolable.New()
112 {
113 disposed = false;
114
115 recursion = Recursion<RecursionNode>.New();
116 }
117
118 public void Dispose()
119 {
120 if (disposed)
121 {
122 throw new ObjectDisposedException(ToString());
123 }
124
125 GenericPool<Flow>.Free(this);
126 }
127
128 void IPoolable.Free()
129 {
130 stack?.Dispose();
131 recursion?.Dispose();
132 locals.Clear();
133 loops.Clear();
134 variables.Clear();
135
136 // Preserved stacks could remain if coroutine was interrupted
137 foreach (var preservedStack in preservedStacks)
138 {
139 preservedStack.Dispose();
140 }
141
142 preservedStacks.Clear();
143
144 loopIdentifier = -1;
145 stack = null;
146 recursion = null;
147 isCoroutine = false;
148 coroutineEnumerator = null;
149 coroutineRunner = null;
150 activeCoroutinesRegistry?.Remove(this);
151 activeCoroutinesRegistry = null;
152 coroutineStopRequested = false;
153 isPrediction = false;
154
155 disposed = true;
156 }
157
158 public GraphStack PreserveStack()
159 {
160 var preservedStack = stack.Clone();
161 preservedStacks.Add(preservedStack);
162 return preservedStack;
163 }
164
165 public void RestoreStack(GraphStack stack)
166 {
167 this.stack.CopyFrom(stack);
168 }
169
170 public void DisposePreservedStack(GraphStack stack)
171 {
172 stack.Dispose();
173 preservedStacks.Remove(stack);
174 }
175
176 #endregion
177
178
179 #region Loops
180
181 public int loopIdentifier = -1;
182
183 public int currentLoop
184 {
185 get
186 {
187 if (loops.Count > 0)
188 {
189 return loops.Peek();
190 }
191 else
192 {
193 return -1;
194 }
195 }
196 }
197
198 public bool LoopIsNotBroken(int loop)
199 {
200 return currentLoop == loop;
201 }
202
203 public int EnterLoop()
204 {
205 var loop = ++loopIdentifier;
206
207 loops.Push(loop);
208
209 return loop;
210 }
211
212 public void BreakLoop()
213 {
214 if (currentLoop < 0)
215 {
216 throw new InvalidOperationException("No active loop to break.");
217 }
218
219 loops.Pop();
220 }
221
222 public void ExitLoop(int loop)
223 {
224 if (loop != currentLoop)
225 {
226 // Already exited through break
227 return;
228 }
229
230 loops.Pop();
231 }
232
233 #endregion
234
235
236 #region Control
237
238 public void Run(ControlOutput port)
239 {
240 Invoke(port);
241 Dispose();
242 }
243
244 public void StartCoroutine(ControlOutput port, ICollection<Flow> registry = null)
245 {
246 isCoroutine = true;
247
248 coroutineRunner = stack.component;
249
250 if (coroutineRunner == null)
251 {
252 coroutineRunner = CoroutineRunner.instance;
253 }
254
255 activeCoroutinesRegistry = registry;
256
257 activeCoroutinesRegistry?.Add(this);
258
259 // We have to store the enumerator because Coroutine itself
260 // can't be cast to IDisposable, which we'll need when stopping.
261 coroutineEnumerator = Coroutine(port);
262
263 coroutineRunner.StartCoroutine(coroutineEnumerator);
264 }
265
266 public void StopCoroutine(bool disposeInstantly)
267 {
268 if (!isCoroutine)
269 {
270 throw new NotSupportedException("Stop may only be called on coroutines.");
271 }
272
273 if (disposeInstantly)
274 {
275 StopCoroutineImmediate();
276 }
277 else
278 {
279 // We prefer a soft coroutine stop here that will happen at the *next frame*,
280 // because we don't want the flow to be disposed just yet when the event node stops
281 // listening, as we still need it for clean up operations.
282 coroutineStopRequested = true;
283 }
284 }
285
286 internal void StopCoroutineImmediate()
287 {
288 if (coroutineRunner && coroutineEnumerator != null)
289 {
290 coroutineRunner.StopCoroutine(coroutineEnumerator);
291
292 // Unity doesn't dispose coroutines enumerators when calling StopCoroutine, so we have to do it manually:
293 // https://forum.unity.com/threads/finally-block-not-executing-in-a-stopped-coroutine.320611/
294 ((IDisposable)coroutineEnumerator).Dispose();
295 }
296 }
297
298 private IEnumerator Coroutine(ControlOutput startPort)
299 {
300 try
301 {
302 foreach (var instruction in InvokeCoroutine(startPort))
303 {
304 if (coroutineStopRequested)
305 {
306 yield break;
307 }
308
309 yield return instruction;
310
311 if (coroutineStopRequested)
312 {
313 yield break;
314 }
315 }
316 }
317 finally
318 {
319 // Manual disposal might have already occurred from StopCoroutine,
320 // so we have to avoid double disposal, which would throw.
321 if (!disposed)
322 {
323 Dispose();
324 }
325 }
326 }
327
328 public void Invoke(ControlOutput output)
329 {
330 Ensure.That(nameof(output)).IsNotNull(output);
331
332 var connection = output.connection;
333
334 if (connection == null)
335 {
336 return;
337 }
338
339 var input = connection.destination;
340
341 var recursionNode = new RecursionNode(output, stack);
342
343 BeforeInvoke(output, recursionNode);
344
345 try
346 {
347 var nextPort = InvokeDelegate(input);
348
349 if (nextPort != null)
350 {
351 Invoke(nextPort);
352 }
353 }
354 finally
355 {
356 AfterInvoke(output, recursionNode);
357 }
358 }
359
360 private IEnumerable InvokeCoroutine(ControlOutput output)
361 {
362 var connection = output.connection;
363
364 if (connection == null)
365 {
366 yield break;
367 }
368
369 var input = connection.destination;
370
371 var recursionNode = new RecursionNode(output, stack);
372
373 BeforeInvoke(output, recursionNode);
374
375 if (input.supportsCoroutine)
376 {
377 foreach (var instruction in InvokeCoroutineDelegate(input))
378 {
379 if (instruction is ControlOutput)
380 {
381 foreach (var unwrappedInstruction in InvokeCoroutine((ControlOutput)instruction))
382 {
383 yield return unwrappedInstruction;
384 }
385 }
386 else
387 {
388 yield return instruction;
389 }
390 }
391 }
392 else
393 {
394 ControlOutput nextPort = InvokeDelegate(input);
395
396 if (nextPort != null)
397 {
398 foreach (var instruction in InvokeCoroutine(nextPort))
399 {
400 yield return instruction;
401 }
402 }
403 }
404
405 AfterInvoke(output, recursionNode);
406 }
407
408 private RecursionNode BeforeInvoke(ControlOutput output, RecursionNode recursionNode)
409 {
410 try
411 {
412 recursion?.Enter(recursionNode);
413 }
414 catch (StackOverflowException ex)
415 {
416 output.unit.HandleException(stack, ex);
417 throw;
418 }
419
420 var connection = output.connection;
421 var input = connection.destination;
422
423 if (enableDebug)
424 {
425 var connectionEditorData = stack.GetElementDebugData<IUnitConnectionDebugData>(connection);
426 var inputUnitEditorData = stack.GetElementDebugData<IUnitDebugData>(input.unit);
427
428 connectionEditorData.lastInvokeFrame = EditorTimeBinding.frame;
429 connectionEditorData.lastInvokeTime = EditorTimeBinding.time;
430 inputUnitEditorData.lastInvokeFrame = EditorTimeBinding.frame;
431 inputUnitEditorData.lastInvokeTime = EditorTimeBinding.time;
432 }
433
434 return recursionNode;
435 }
436
437 private void AfterInvoke(ControlOutput output, RecursionNode recursionNode)
438 {
439 recursion?.Exit(recursionNode);
440 }
441
442 private ControlOutput InvokeDelegate(ControlInput input)
443 {
444 try
445 {
446 if (input.requiresCoroutine)
447 {
448 throw new InvalidOperationException($"Port '{input.key}' on '{input.unit}' can only be triggered in a coroutine.");
449 }
450
451 return input.action(this);
452 }
453 catch (Exception ex)
454 {
455 input.unit.HandleException(stack, ex);
456 throw;
457 }
458 }
459
460 private IEnumerable InvokeCoroutineDelegate(ControlInput input)
461 {
462 var instructions = input.coroutineAction(this);
463
464 while (true)
465 {
466 object instruction;
467
468 try
469 {
470 if (!instructions.MoveNext())
471 {
472 break;
473 }
474
475 instruction = instructions.Current;
476 }
477 catch (Exception ex)
478 {
479 input.unit.HandleException(stack, ex);
480 throw;
481 }
482
483 yield return instruction;
484 }
485 }
486
487 #endregion
488
489
490 #region Values
491
492 public bool IsLocal(IUnitValuePort port)
493 {
494 Ensure.That(nameof(port)).IsNotNull(port);
495
496 return locals.ContainsKey(port);
497 }
498
499 public void SetValue(IUnitValuePort port, object value)
500 {
501 Ensure.That(nameof(port)).IsNotNull(port);
502 Ensure.That(nameof(value)).IsOfType(value, port.type);
503
504 if (locals.ContainsKey(port))
505 {
506 locals[port] = value;
507 }
508 else
509 {
510 locals.Add(port, value);
511 }
512 }
513
514 public object GetValue(ValueInput input)
515 {
516 if (locals.TryGetValue(input, out var local))
517 {
518 return local;
519 }
520
521 var connection = input.connection;
522
523 if (connection != null)
524 {
525 if (enableDebug)
526 {
527 var connectionEditorData = stack.GetElementDebugData<IUnitConnectionDebugData>(connection);
528
529 connectionEditorData.lastInvokeFrame = EditorTimeBinding.frame;
530 connectionEditorData.lastInvokeTime = EditorTimeBinding.time;
531 }
532
533 var output = connection.source;
534
535 var value = GetValue(output);
536
537 if (enableDebug)
538 {
539 var connectionEditorData = stack.GetElementDebugData<ValueConnection.DebugData>(connection);
540
541 connectionEditorData.lastValue = value;
542 connectionEditorData.assignedLastValue = true;
543 }
544
545 return value;
546 }
547 else if (TryGetDefaultValue(input, out var defaultValue))
548 {
549 return defaultValue;
550 }
551 else
552 {
553 throw new MissingValuePortInputException(input.key);
554 }
555 }
556
557 private object GetValue(ValueOutput output)
558 {
559 if (locals.TryGetValue(output, out var local))
560 {
561 return local;
562 }
563
564 if (!output.supportsFetch)
565 {
566 throw new InvalidOperationException($"The value of '{output.key}' on '{output.unit}' cannot be fetched dynamically, it must be assigned.");
567 }
568
569 var recursionNode = new RecursionNode(output, stack);
570
571 try
572 {
573 recursion?.Enter(recursionNode);
574 }
575 catch (StackOverflowException ex)
576 {
577 output.unit.HandleException(stack, ex);
578 throw;
579 }
580
581 try
582 {
583 if (enableDebug)
584 {
585 var outputUnitEditorData = stack.GetElementDebugData<IUnitDebugData>(output.unit);
586
587 outputUnitEditorData.lastInvokeFrame = EditorTimeBinding.frame;
588 outputUnitEditorData.lastInvokeTime = EditorTimeBinding.time;
589 }
590
591 var value = GetValueDelegate(output);
592
593 return value;
594 }
595 finally
596 {
597 recursion?.Exit(recursionNode);
598 }
599 }
600
601 public object GetValue(ValueInput input, Type type)
602 {
603 return ConversionUtility.Convert(GetValue(input), type);
604 }
605
606 public T GetValue<T>(ValueInput input)
607 {
608 return (T)GetValue(input, typeof(T));
609 }
610
611 public object GetConvertedValue(ValueInput input)
612 {
613 return GetValue(input, input.type);
614 }
615
616 private object GetDefaultValue(ValueInput input)
617 {
618 if (!TryGetDefaultValue(input, out var defaultValue))
619 {
620 throw new InvalidOperationException("Value input port does not have a default value.");
621 }
622
623 return defaultValue;
624 }
625
626 public bool TryGetDefaultValue(ValueInput input, out object defaultValue)
627 {
628 if (!input.unit.defaultValues.TryGetValue(input.key, out defaultValue))
629 {
630 return false;
631 }
632
633 if (input.nullMeansSelf && defaultValue == null)
634 {
635 defaultValue = stack.self;
636 }
637
638 return true;
639 }
640
641 private object GetValueDelegate(ValueOutput output)
642 {
643 try
644 {
645 return output.getValue(this);
646 }
647 catch (Exception ex)
648 {
649 output.unit.HandleException(stack, ex);
650 throw;
651 }
652 }
653
654 public static object FetchValue(ValueInput input, GraphReference reference)
655 {
656 var flow = New(reference);
657
658 var result = flow.GetValue(input);
659
660 flow.Dispose();
661
662 return result;
663 }
664
665 public static object FetchValue(ValueInput input, Type type, GraphReference reference)
666 {
667 return ConversionUtility.Convert(FetchValue(input, reference), type);
668 }
669
670 public static T FetchValue<T>(ValueInput input, GraphReference reference)
671 {
672 return (T)FetchValue(input, typeof(T), reference);
673 }
674
675 #endregion
676
677
678 #region Value Prediction
679
680 public static bool CanPredict(IUnitValuePort port, GraphReference reference)
681 {
682 Ensure.That(nameof(port)).IsNotNull(port);
683
684 var flow = New(reference);
685
686 flow.isPrediction = true;
687
688 bool canPredict;
689
690 if (port is ValueInput)
691 {
692 canPredict = flow.CanPredict((ValueInput)port);
693 }
694 else if (port is ValueOutput)
695 {
696 canPredict = flow.CanPredict((ValueOutput)port);
697 }
698 else
699 {
700 throw new NotSupportedException();
701 }
702
703 flow.Dispose();
704
705 return canPredict;
706 }
707
708 private bool CanPredict(ValueInput input)
709 {
710 if (!input.hasValidConnection)
711 {
712 if (!TryGetDefaultValue(input, out var defaultValue))
713 {
714 return false;
715 }
716
717 if (typeof(Component).IsAssignableFrom(input.type))
718 {
719 defaultValue = defaultValue?.ConvertTo(input.type);
720 }
721
722 if (!input.allowsNull && defaultValue == null)
723 {
724 return false;
725 }
726
727 return true;
728 }
729
730 var output = input.validConnectedPorts.Single();
731
732 if (!CanPredict(output))
733 {
734 return false;
735 }
736
737 var connectedValue = GetValue(output);
738
739 if (!ConversionUtility.CanConvert(connectedValue, input.type, false))
740 {
741 return false;
742 }
743
744 if (typeof(Component).IsAssignableFrom(input.type))
745 {
746 connectedValue = connectedValue?.ConvertTo(input.type);
747 }
748
749 if (!input.allowsNull && connectedValue == null)
750 {
751 return false;
752 }
753
754 return true;
755 }
756
757 private bool CanPredict(ValueOutput output)
758 {
759 // Shortcircuit the expensive check if the port isn't marked as predictable
760 if (!output.supportsPrediction)
761 {
762 return false;
763 }
764
765 var recursionNode = new RecursionNode(output, stack);
766
767 if (!recursion?.TryEnter(recursionNode) ?? false)
768 {
769 return false;
770 }
771
772 // Check each value dependency
773 foreach (var relation in output.unit.relations.WithDestination(output))
774 {
775 if (relation.source is ValueInput)
776 {
777 var source = (ValueInput)relation.source;
778
779 if (!CanPredict(source))
780 {
781 recursion?.Exit(recursionNode);
782 return false;
783 }
784 }
785 }
786
787 var value = CanPredictDelegate(output);
788
789 recursion?.Exit(recursionNode);
790
791 return value;
792 }
793
794 private bool CanPredictDelegate(ValueOutput output)
795 {
796 try
797 {
798 return output.canPredictValue(this);
799 }
800 catch (Exception ex)
801 {
802 Debug.LogWarning($"Prediction check failed for '{output.key}' on '{output.unit}':\n{ex}");
803
804 return false;
805 }
806 }
807
808 public static object Predict(IUnitValuePort port, GraphReference reference)
809 {
810 Ensure.That(nameof(port)).IsNotNull(port);
811
812 var flow = New(reference);
813
814 flow.isPrediction = true;
815
816 object value;
817
818 if (port is ValueInput)
819 {
820 value = flow.GetValue((ValueInput)port);
821 }
822 else if (port is ValueOutput)
823 {
824 value = flow.GetValue((ValueOutput)port);
825 }
826 else
827 {
828 throw new NotSupportedException();
829 }
830
831 flow.Dispose();
832
833 return value;
834 }
835
836 public static object Predict(IUnitValuePort port, GraphReference reference, Type type)
837 {
838 return ConversionUtility.Convert(Predict(port, reference), type);
839 }
840
841 public static T Predict<T>(IUnitValuePort port, GraphReference pointer)
842 {
843 return (T)Predict(port, pointer, typeof(T));
844 }
845
846 #endregion
847 }
848}