A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEngine;
4
5namespace Unity.VisualScripting
6{
7 [SerializationVersion("A")]
8 [SpecialUnit]
9 public abstract class EventUnit<TArgs> : Unit, IEventUnit, IGraphElementWithData, IGraphEventHandler<TArgs>
10 {
11 public class Data : IGraphElementData
12 {
13 public EventHook hook;
14
15 public Delegate handler;
16
17 public bool isListening;
18
19 public HashSet<Flow> activeCoroutines = new HashSet<Flow>();
20 }
21
22 public virtual IGraphElementData CreateData()
23 {
24 return new Data();
25 }
26
27 /// <summary>
28 /// Run this event in a coroutine, enabling asynchronous flow like wait nodes.
29 /// </summary>
30 [Serialize]
31 [Inspectable]
32 [InspectorExpandTooltip]
33 public bool coroutine { get; set; } = false;
34
35 [DoNotSerialize]
36 [PortLabelHidden]
37 public ControlOutput trigger { get; private set; }
38
39 [DoNotSerialize]
40 protected abstract bool register { get; }
41
42 protected override void Definition()
43 {
44 isControlRoot = true;
45
46 trigger = ControlOutput(nameof(trigger));
47 }
48
49 public virtual EventHook GetHook(GraphReference reference)
50 {
51 throw new InvalidImplementationException($"Missing event hook for '{this}'.");
52 }
53
54 public virtual void StartListening(GraphStack stack)
55 {
56 var data = stack.GetElementData<Data>(this);
57
58 if (data.isListening)
59 {
60 return;
61 }
62
63 if (register)
64 {
65 var reference = stack.ToReference();
66 var hook = GetHook(reference);
67 Action<TArgs> handler = args => Trigger(reference, args);
68 EventBus.Register(hook, handler);
69
70 data.hook = hook;
71 data.handler = handler;
72 }
73
74 data.isListening = true;
75 }
76
77 public virtual void StopListening(GraphStack stack)
78 {
79 var data = stack.GetElementData<Data>(this);
80
81 if (!data.isListening)
82 {
83 return;
84 }
85
86 // The coroutine's flow will dispose at the next frame, letting us
87 // keep the current flow for clean up operations if needed
88 foreach (var activeCoroutine in data.activeCoroutines)
89 {
90 activeCoroutine.StopCoroutine(false);
91 }
92
93 if (register)
94 {
95 EventBus.Unregister(data.hook, data.handler);
96
97 stack.ClearReference();
98
99 data.handler = null;
100 }
101
102 data.isListening = false;
103 }
104
105 public override void Uninstantiate(GraphReference instance)
106 {
107 // Here, we're relying on the fact that OnDestroy calls Uninstantiate.
108 // We need to force-dispose any remaining coroutine to avoid
109 // memory leaks, because OnDestroy on the runner will not keep
110 // executing MoveNext() until our soft-destroy call at the end of Flow.Coroutine
111 // or even dispose the coroutine's enumerator (!).
112 var data = instance.GetElementData<Data>(this);
113 var coroutines = data.activeCoroutines.ToHashSetPooled();
114
115#if UNITY_EDITOR
116 new FrameDelayedCallback(() => StopAllCoroutines(coroutines), 1);
117#else
118 StopAllCoroutines(coroutines);
119#endif
120
121 base.Uninstantiate(instance);
122 }
123
124 static void StopAllCoroutines(HashSet<Flow> activeCoroutines)
125 {
126 // The coroutine's flow will dispose instantly, thus modifying
127 // the activeCoroutines registry while we enumerate over it
128 foreach (var activeCoroutine in activeCoroutines)
129 {
130 activeCoroutine.StopCoroutineImmediate();
131 }
132 activeCoroutines.Free();
133 }
134
135 public bool IsListening(GraphPointer pointer)
136 {
137 if (!pointer.hasData)
138 {
139 return false;
140 }
141
142 return pointer.GetElementData<Data>(this).isListening;
143 }
144
145 public void Trigger(GraphReference reference, TArgs args)
146 {
147 InternalTrigger(reference, args);
148 }
149
150 private protected virtual void InternalTrigger(GraphReference reference, TArgs args)
151 {
152 var flow = Flow.New(reference);
153
154 if (!ShouldTrigger(flow, args))
155 {
156 flow.Dispose();
157 return;
158 }
159
160 AssignArguments(flow, args);
161
162 Run(flow);
163 }
164
165 protected virtual bool ShouldTrigger(Flow flow, TArgs args)
166 {
167 return true;
168 }
169
170 protected virtual void AssignArguments(Flow flow, TArgs args)
171 {
172 }
173
174 private void Run(Flow flow)
175 {
176 if (flow.enableDebug)
177 {
178 var editorData = flow.stack.GetElementDebugData<IUnitDebugData>(this);
179
180 editorData.lastInvokeFrame = EditorTimeBinding.frame;
181 editorData.lastInvokeTime = EditorTimeBinding.time;
182 }
183
184 if (coroutine)
185 {
186 flow.StartCoroutine(trigger, flow.stack.GetElementData<Data>(this).activeCoroutines);
187 }
188 else
189 {
190 flow.Run(trigger);
191 }
192 }
193
194 protected static bool CompareNames(Flow flow, ValueInput namePort, string calledName)
195 {
196 Ensure.That(nameof(calledName)).IsNotNull(calledName);
197
198 return calledName.Trim().Equals(flow.GetValue<string>(namePort)?.Trim(), StringComparison.OrdinalIgnoreCase);
199 }
200 }
201}