A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5using UnityEngine;
6using UnityEngine.SceneManagement;
7using UnityObject = UnityEngine.Object;
8
9namespace Unity.VisualScripting
10{
11 public abstract class GraphPointer
12 {
13 #region Lifecycle
14
15 protected static bool IsValidRoot(IGraphRoot root)
16 {
17 return root?.childGraph != null && root as UnityObject != null;
18 }
19
20 protected static bool IsValidRoot(UnityObject rootObject)
21 {
22 return rootObject != null && (rootObject as IGraphRoot)?.childGraph != null;
23 }
24
25 internal GraphPointer() { }
26
27 protected void Initialize(IGraphRoot root)
28 {
29 if (!IsValidRoot(root))
30 {
31 throw new ArgumentException("Graph pointer root must be a valid Unity object with a non-null child graph.", nameof(root));
32 }
33
34 if (!(root is IMachine && root is MonoBehaviour || root is IMacro && root is ScriptableObject))
35 {
36 throw new ArgumentException("Graph pointer root must be either a machine or a macro.", nameof(root));
37 }
38
39 this.root = root;
40
41 parentStack.Add(root);
42
43 graphStack.Add(root.childGraph);
44
45 dataStack.Add(machine?.graphData);
46
47 debugDataStack.Add(fetchRootDebugDataBinding?.Invoke(root));
48
49 if (machine != null)
50 {
51 // Annoyingly, getting the gameObject property is an API call
52 // First, we'll try using our IMachine safe reference that is assigned in play mode on Awake
53 // If that fails, we'll try fetching it dynamically
54
55 if (machine.threadSafeGameObject != null)
56 {
57 gameObject = machine.threadSafeGameObject;
58 }
59 else if (UnityThread.allowsAPI)
60 {
61 gameObject = component.gameObject;
62 }
63 else
64 {
65 throw new GraphPointerException("Could not fetch graph pointer root game object.", this);
66 }
67 }
68 else
69 {
70 gameObject = null;
71 }
72 }
73
74 protected void Initialize(IGraphRoot root, IEnumerable<IGraphParentElement> parentElements, bool ensureValid)
75 {
76 Initialize(root);
77
78 Ensure.That(nameof(parentElements)).IsNotNull(parentElements);
79
80 foreach (var parentElement in parentElements)
81 {
82 if (!TryEnterParentElement(parentElement, out var error))
83 {
84 if (ensureValid)
85 {
86 throw new GraphPointerException(error, this);
87 }
88
89 break;
90 }
91 }
92 }
93
94 protected void Initialize(UnityObject rootObject, IEnumerable<Guid> parentElementGuids, bool ensureValid)
95 {
96 Initialize(rootObject as IGraphRoot);
97
98 Ensure.That(nameof(parentElementGuids)).IsNotNull(parentElementGuids);
99
100 foreach (var parentElementGuid in parentElementGuids)
101 {
102 if (!TryEnterParentElement(parentElementGuid, out var error))
103 {
104 if (ensureValid)
105 {
106 throw new GraphPointerException(error, this);
107 }
108
109 break;
110 }
111 }
112 }
113
114 #endregion
115
116
117 #region Conversion
118
119 public abstract GraphReference AsReference();
120
121 public virtual void CopyFrom(GraphPointer other)
122 {
123 root = other.root;
124 gameObject = other.gameObject;
125
126 parentStack.Clear();
127 parentElementStack.Clear();
128 graphStack.Clear();
129 dataStack.Clear();
130 debugDataStack.Clear();
131
132 foreach (var parent in other.parentStack)
133 {
134 parentStack.Add(parent);
135 }
136
137 foreach (var parentElement in other.parentElementStack)
138 {
139 parentElementStack.Add(parentElement);
140 }
141
142 foreach (var graph in other.graphStack)
143 {
144 graphStack.Add(graph);
145 }
146
147 foreach (var data in other.dataStack)
148 {
149 dataStack.Add(data);
150 }
151
152 foreach (var debugData in other.debugDataStack)
153 {
154 debugDataStack.Add(debugData);
155 }
156 }
157
158 #endregion
159
160
161 #region Stack
162
163 public IGraphRoot root { get; protected set; }
164
165 public UnityObject rootObject => root as UnityObject;
166
167 public IMachine machine => root as IMachine;
168
169 public IMacro macro => root as IMacro;
170
171 public MonoBehaviour component => root as MonoBehaviour;
172
173 public GameObject gameObject { get; private set; }
174
175 public GameObject self => gameObject;
176
177 public ScriptableObject scriptableObject => root as ScriptableObject;
178
179 public Scene? scene
180 {
181 get
182 {
183 if (gameObject == null)
184 {
185 return null;
186 }
187
188 var scene = gameObject.scene;
189
190 // We must allow to return unloaded scenes, because
191 // On Enable might try fetching scene variables for example
192 // See: https://support.ludiq.io/communities/5/topics/1864-/
193
194 if (!scene.IsValid() /* || !scene.isLoaded */)
195 {
196 return null;
197 }
198
199 return scene;
200 }
201 }
202
203 public UnityObject serializedObject
204 {
205 get
206 {
207 var depth = this.depth;
208
209 while (depth > 0)
210 {
211 var parent = parentStack[depth - 1];
212
213 if (parent.isSerializationRoot)
214 {
215 return parent.serializedObject;
216 }
217
218 depth--;
219 }
220
221 throw new GraphPointerException("Could not find serialized object.", this);
222 }
223 }
224
225 protected readonly List<IGraphParent> parentStack = new List<IGraphParent>();
226
227 protected readonly List<IGraphParentElement> parentElementStack = new List<IGraphParentElement>();
228
229 protected readonly List<IGraph> graphStack = new List<IGraph>();
230
231 protected readonly List<IGraphData> dataStack = new List<IGraphData>();
232
233 protected readonly List<IGraphDebugData> debugDataStack = new List<IGraphDebugData>();
234
235 public IEnumerable<Guid> parentElementGuids => parentElementStack.Select(parentElement => parentElement.guid);
236
237 #endregion
238
239
240 #region Utility
241
242 public int depth => parentStack.Count;
243
244 public bool isRoot => depth == 1;
245
246 public bool isChild => depth > 1;
247
248 public void EnsureDepthValid(int depth)
249 {
250 Ensure.That(nameof(depth)).IsGte(depth, 1);
251
252 if (depth > this.depth)
253 {
254 throw new GraphPointerException($"Trying to fetch a graph pointer level above depth: {depth} > {this.depth}", this);
255 }
256 }
257
258 public void EnsureChild()
259 {
260 if (!isChild)
261 {
262 throw new GraphPointerException("Graph pointer does not point to a child graph.", this);
263 }
264 }
265
266 public bool IsWithin<T>() where T : IGraphParent
267 {
268 return parent is T;
269 }
270
271 public void EnsureWithin<T>() where T : IGraphParent
272 {
273 if (!IsWithin<T>())
274 {
275 throw new GraphPointerException($"Graph pointer must be within a {typeof(T)} for this operation.", this);
276 }
277 }
278
279 public IGraphParent parent => parentStack[parentStack.Count - 1];
280
281 public T GetParent<T>() where T : IGraphParent
282 {
283 EnsureWithin<T>();
284
285 return (T)parent;
286 }
287
288 public IGraphParentElement parentElement
289 {
290 get
291 {
292 EnsureChild();
293
294 return parentElementStack[parentElementStack.Count - 1];
295 }
296 }
297
298 public IGraph rootGraph => graphStack[0];
299
300 public IGraph graph => graphStack[graphStack.Count - 1];
301
302 protected IGraphData _data
303 {
304 get => dataStack[dataStack.Count - 1];
305 set => dataStack[dataStack.Count - 1] = value;
306 }
307
308 public IGraphData data
309 {
310 get
311 {
312 EnsureDataAvailable();
313 return _data;
314 }
315 }
316
317 protected IGraphData _parentData => dataStack[dataStack.Count - 2];
318
319 public bool hasData => _data != null;
320
321 public void EnsureDataAvailable()
322 {
323 if (!hasData)
324 {
325 throw new GraphPointerException($"Graph data is not available.", this);
326 }
327 }
328
329 public T GetGraphData<T>() where T : IGraphData
330 {
331 var data = this.data;
332
333 if (data is T)
334 {
335 return (T)data;
336 }
337
338 throw new GraphPointerException($"Graph data type mismatch. Found {data.GetType()}, expected {typeof(T)}.", this);
339 }
340
341 public T GetElementData<T>(IGraphElementWithData element) where T : IGraphElementData
342 {
343 if (_data.TryGetElementData(element, out var elementData))
344 {
345 if (elementData is T)
346 {
347 return (T)elementData;
348 }
349
350 throw new GraphPointerException($"Graph element data type mismatch. Found {elementData.GetType()}, expected {typeof(T)}.", this);
351 }
352
353 throw new GraphPointerException($"Missing graph element data for {element}.", this);
354 }
355
356 public static Func<IGraphRoot, IGraphDebugData> fetchRootDebugDataBinding { get; set; }
357 internal static Action<IGraphRoot> releaseDebugDataBinding;
358
359 public bool hasDebugData => _debugData != null;
360
361 public void EnsureDebugDataAvailable()
362 {
363 if (!hasDebugData)
364 {
365 throw new GraphPointerException($"Graph debug data is not available.", this);
366 }
367 }
368
369 protected IGraphDebugData _debugData
370 {
371 get => debugDataStack[debugDataStack.Count - 1];
372 set => debugDataStack[debugDataStack.Count - 1] = value;
373 }
374
375 public IGraphDebugData debugData
376 {
377 get
378 {
379 EnsureDebugDataAvailable();
380 return _debugData;
381 }
382 }
383
384 public T GetGraphDebugData<T>() where T : IGraphDebugData
385 {
386 var debugData = this.debugData;
387
388 if (debugData is T)
389 {
390 return (T)debugData;
391 }
392
393 throw new GraphPointerException($"Graph debug data type mismatch. Found {debugData.GetType()}, expected {typeof(T)}.", this);
394 }
395
396 public T GetElementDebugData<T>(IGraphElementWithDebugData element)
397 {
398 var elementDebugData = debugData.GetOrCreateElementData(element);
399
400 if (elementDebugData is T)
401 {
402 return (T)elementDebugData;
403 }
404
405 throw new GraphPointerException($"Graph element runtime debug data type mismatch. Found {elementDebugData.GetType()}, expected {typeof(T)}.", this);
406 }
407
408 #endregion
409
410
411 #region Traversal
412
413 protected bool TryEnterParentElement(Guid parentElementGuid, out string error, int? maxRecursionDepth = null)
414 {
415 if (!graph.elements.TryGetValue(parentElementGuid, out var element))
416 {
417 error = "Trying to enter a graph parent element with a GUID that is not within the current graph.";
418 return false;
419 }
420
421 if (!(element is IGraphParentElement))
422 {
423 error = "Provided element GUID does not point to a graph parent element.";
424 return false;
425 }
426
427 var parentElement = (IGraphParentElement)element;
428
429 return TryEnterParentElement(parentElement, out error, maxRecursionDepth);
430 }
431
432 protected bool TryEnterParentElement(IGraphParentElement parentElement, out string error, int? maxRecursionDepth = null, bool skipContainsCheck = false)
433 {
434 // The contains check is expensive because variant+merged collection checks
435 // If we already know for sure this error cannot happen, skipping it provides a significant optim
436 if (!skipContainsCheck && !graph.elements.Contains(parentElement))
437 {
438 error = "Trying to enter a graph parent element that is not within the current graph.";
439 return false;
440 }
441
442 var childGraph = parentElement.childGraph;
443
444 if (childGraph == null)
445 {
446 error = "Trying to enter a graph parent element without a child graph.";
447 return false;
448 }
449
450 if (Recursion.safeMode)
451 {
452 var recursionDepth = 0;
453 var _maxRecursionDepth = maxRecursionDepth ?? Recursion.defaultMaxDepth;
454
455 foreach (var parentGraph in graphStack)
456 {
457 if (parentGraph == childGraph)
458 {
459 recursionDepth++;
460 }
461 }
462
463 if (recursionDepth > _maxRecursionDepth)
464 {
465 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)}'.";
466 return false;
467 }
468 }
469
470 EnterValidParentElement(parentElement);
471 error = null;
472 return true;
473 }
474
475 protected void EnterParentElement(IGraphParentElement parentElement)
476 {
477 if (!TryEnterParentElement(parentElement, out var error))
478 {
479 throw new GraphPointerException(error, this);
480 }
481 }
482
483 protected void EnterParentElement(Guid parentElementGuid)
484 {
485 if (!TryEnterParentElement(parentElementGuid, out var error))
486 {
487 throw new GraphPointerException(error, this);
488 }
489 }
490
491 private void EnterValidParentElement(IGraphParentElement parentElement)
492 {
493 var childGraph = parentElement.childGraph;
494
495 parentStack.Add(parentElement);
496 parentElementStack.Add(parentElement);
497 graphStack.Add(childGraph);
498
499 IGraphData childGraphData = null;
500 _data?.TryGetChildGraphData(parentElement, out childGraphData);
501 dataStack.Add(childGraphData);
502
503 var childGraphDebugData = _debugData?.GetOrCreateChildGraphData(parentElement);
504 debugDataStack.Add(childGraphDebugData);
505 }
506
507 protected void ExitParentElement()
508 {
509 if (!isChild)
510 {
511 throw new GraphPointerException("Trying to exit the root graph.", this);
512 }
513
514 parentStack.RemoveAt(parentStack.Count - 1);
515 parentElementStack.RemoveAt(parentElementStack.Count - 1);
516 graphStack.RemoveAt(graphStack.Count - 1);
517 dataStack.RemoveAt(dataStack.Count - 1);
518 debugDataStack.RemoveAt(debugDataStack.Count - 1);
519 }
520
521 #endregion
522
523
524 #region Validation
525
526 public bool isValid
527 {
528 get
529 {
530 try
531 {
532 if (rootObject == null)
533 {
534 // Root object has been destroyed
535 return false;
536 }
537
538 if (rootGraph != root.childGraph)
539 {
540 // Root graph has changed
541 return false;
542 }
543
544 if (serializedObject == null)
545 {
546 // Serialized object has been destroyed
547 return false;
548 }
549
550 for (var depth = 1; depth < this.depth; depth++)
551 {
552 var parentElement = parentElementStack[depth - 1];
553 var parentGraph = graphStack[depth - 1];
554 var childGraph = graphStack[depth];
555
556 // Important to check by object and not by GUID here,
557 // because object stack integrity has to be guaranteed
558 // (GUID integrity is implied because they're immutable)
559 if (!parentGraph.elements.Contains(parentElement))
560 {
561 // Parent graph no longer contains the parent element
562 return false;
563 }
564
565 if (parentElement.childGraph != childGraph)
566 {
567 // Child graph has changed
568 return false;
569 }
570 }
571
572 return true;
573 }
574 catch (Exception ex)
575 {
576 Debug.LogWarning("Failed to check graph pointer validity: \n" + ex);
577 return false;
578 }
579 }
580 }
581
582 public void EnsureValid()
583 {
584 if (!isValid)
585 {
586 throw new GraphPointerException("Graph pointer is invalid.", this);
587 }
588 }
589
590 #endregion
591
592
593 #region Equality
594
595 public bool InstanceEquals(GraphPointer other)
596 {
597 if (ReferenceEquals(this, other))
598 {
599 return true;
600 }
601
602 if (!UnityObjectUtility.TrulyEqual(rootObject, other.rootObject))
603 {
604 return false;
605 }
606
607 if (!DefinitionEquals(other))
608 {
609 return false;
610 }
611
612 var depth = this.depth; // Micro optimization
613
614 for (int d = 0; d < depth; d++)
615 {
616 var data = dataStack[d];
617 var otherData = other.dataStack[d];
618
619 if (data != otherData)
620 {
621 return false;
622 }
623 }
624
625 return true;
626 }
627
628 public bool DefinitionEquals(GraphPointer other)
629 {
630 if (other == null)
631 {
632 return false;
633 }
634
635 if (rootGraph != other.rootGraph)
636 {
637 return false;
638 }
639
640 var depth = this.depth; // Micro optimization
641
642 if (depth != other.depth)
643 {
644 return false;
645 }
646
647 for (int d = 1; d < depth; d++)
648 {
649 var parentElement = parentElementStack[d - 1];
650 var otherParentElement = other.parentElementStack[d - 1];
651
652 if (parentElement != otherParentElement)
653 {
654 return false;
655 }
656 }
657
658 return true;
659 }
660
661 public int ComputeHashCode()
662 {
663 var hashCode = 17;
664
665 hashCode = hashCode * 23 + (rootObject.AsUnityNull()?.GetHashCode() ?? 0);
666
667 hashCode = hashCode * 23 + (rootGraph?.GetHashCode() ?? 0);
668
669 var depth = this.depth; // Micro optimization
670
671 for (int d = 1; d < depth; d++)
672 {
673 var parentElementGuid = parentElementStack[d - 1].guid;
674
675 hashCode = hashCode * 23 + parentElementGuid.GetHashCode();
676 }
677
678 return hashCode;
679 }
680
681 #endregion
682
683
684 #region Breadcrumbs
685
686 public override string ToString()
687 {
688 var sb = new StringBuilder();
689
690 sb.Append("[ ");
691
692 sb.Append(rootObject.ToSafeString());
693
694 for (var depth = 1; depth < this.depth; depth++)
695 {
696 sb.Append(" > ");
697
698 var parentElementIndex = depth - 1;
699
700 if (parentElementIndex >= parentElementStack.Count)
701 {
702 sb.Append("?");
703 break;
704 }
705
706 var parentElement = parentElementStack[parentElementIndex];
707
708 sb.Append(parentElement);
709 }
710
711 sb.Append(" ]");
712
713 return sb.ToString();
714 }
715
716 #endregion
717 }
718}