File size: 5,632 Bytes
18a519f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Unity.VisualScripting
{
    [SerializationVersion("A")]
    [SpecialUnit]
    public abstract class EventUnit<TArgs> : Unit, IEventUnit, IGraphElementWithData, IGraphEventHandler<TArgs>
    {
        public class Data : IGraphElementData
        {
            public EventHook hook;

            public Delegate handler;

            public bool isListening;

            public HashSet<Flow> activeCoroutines = new HashSet<Flow>();
        }

        public virtual IGraphElementData CreateData()
        {
            return new Data();
        }

        /// <summary>
        /// Run this event in a coroutine, enabling asynchronous flow like wait nodes.
        /// </summary>
        [Serialize]
        [Inspectable]
        [InspectorExpandTooltip]
        public bool coroutine { get; set; } = false;

        [DoNotSerialize]
        [PortLabelHidden]
        public ControlOutput trigger { get; private set; }

        [DoNotSerialize]
        protected abstract bool register { get; }

        protected override void Definition()
        {
            isControlRoot = true;

            trigger = ControlOutput(nameof(trigger));
        }

        public virtual EventHook GetHook(GraphReference reference)
        {
            throw new InvalidImplementationException($"Missing event hook for '{this}'.");
        }

        public virtual void StartListening(GraphStack stack)
        {
            var data = stack.GetElementData<Data>(this);

            if (data.isListening)
            {
                return;
            }

            if (register)
            {
                var reference = stack.ToReference();
                var hook = GetHook(reference);
                Action<TArgs> handler = args => Trigger(reference, args);
                EventBus.Register(hook, handler);

                data.hook = hook;
                data.handler = handler;
            }

            data.isListening = true;
        }

        public virtual void StopListening(GraphStack stack)
        {
            var data = stack.GetElementData<Data>(this);

            if (!data.isListening)
            {
                return;
            }

            // The coroutine's flow will dispose at the next frame, letting us
            // keep the current flow for clean up operations if needed
            foreach (var activeCoroutine in data.activeCoroutines)
            {
                activeCoroutine.StopCoroutine(false);
            }

            if (register)
            {
                EventBus.Unregister(data.hook, data.handler);
                data.handler = null;
            }

            data.isListening = false;
        }

        public override void Uninstantiate(GraphReference instance)
        {
            // Here, we're relying on the fact that OnDestroy calls Uninstantiate.
            // We need to force-dispose any remaining coroutine to avoid
            // memory leaks, because OnDestroy on the runner will not keep
            // executing MoveNext() until our soft-destroy call at the end of Flow.Coroutine
            // or even dispose the coroutine's enumerator (!).
            var data = instance.GetElementData<Data>(this);
            var coroutines = data.activeCoroutines.ToHashSetPooled();

#if UNITY_EDITOR
            new FrameDelayedCallback(() => StopAllCoroutines(coroutines), 1);
#else
            StopAllCoroutines(coroutines);
#endif

            base.Uninstantiate(instance);
        }

        static void StopAllCoroutines(HashSet<Flow> activeCoroutines)
        {
            // The coroutine's flow will dispose instantly, thus modifying
            // the activeCoroutines registry while we enumerate over it
            foreach (var activeCoroutine in activeCoroutines)
            {
                activeCoroutine.StopCoroutineImmediate();
            }
            activeCoroutines.Free();
        }

        public bool IsListening(GraphPointer pointer)
        {
            if (!pointer.hasData)
            {
                return false;
            }

            return pointer.GetElementData<Data>(this).isListening;
        }

        public void Trigger(GraphReference reference, TArgs args)
        {
            var flow = Flow.New(reference);

            if (!ShouldTrigger(flow, args))
            {
                flow.Dispose();
                return;
            }

            AssignArguments(flow, args);

            Run(flow);
        }

        protected virtual bool ShouldTrigger(Flow flow, TArgs args)
        {
            return true;
        }

        protected virtual void AssignArguments(Flow flow, TArgs args)
        {
        }

        private void Run(Flow flow)
        {
            if (flow.enableDebug)
            {
                var editorData = flow.stack.GetElementDebugData<IUnitDebugData>(this);

                editorData.lastInvokeFrame = EditorTimeBinding.frame;
                editorData.lastInvokeTime = EditorTimeBinding.time;
            }

            if (coroutine)
            {
                flow.StartCoroutine(trigger, flow.stack.GetElementData<Data>(this).activeCoroutines);
            }
            else
            {
                flow.Run(trigger);
            }
        }

        protected static bool CompareNames(Flow flow, ValueInput namePort, string calledName)
        {
            Ensure.That(nameof(calledName)).IsNotNull(calledName);

            return calledName.Trim().Equals(flow.GetValue<string>(namePort)?.Trim(), StringComparison.OrdinalIgnoreCase);
        }
    }
}