File size: 12,739 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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Unity.VisualScripting
{
    [Analyser(typeof(IUnit))]
    public class UnitAnalyser<TUnit> : Analyser<TUnit, UnitAnalysis>
        where TUnit : class, IUnit
    {
        public UnitAnalyser(GraphReference reference, TUnit target) : base(reference, target) { }

        public TUnit unit => target;

        [Assigns]
        protected bool IsEntered()
        {
            using (var recursion = Recursion.New(1))
            {
                return IsEntered(unit, recursion);
            }
        }

        private static bool IsEntered(IUnit unit, Recursion recursion)
        {
            if (unit.isControlRoot)
            {
                return true;
            }

            foreach (var controlInput in unit.controlInputs)
            {
                if (!controlInput.isPredictable || controlInput.couldBeEntered)
                {
                    return true;
                }
            }

            foreach (var valueOutput in unit.valueOutputs)
            {
                if (!recursion?.TryEnter(valueOutput) ?? false)
                {
                    continue;
                }

                var valueOutputEntered = valueOutput.validConnections.Any(c => IsEntered(c.destination.unit, recursion));

                recursion?.Exit(valueOutput);

                if (valueOutputEntered)
                {
                    return true;
                }
            }

            return false;
        }

        private string PortLabel(IUnitPort port)
        {
            return port.Description<UnitPortDescription>().label;
        }

        [Assigns]
        protected virtual IEnumerable<Warning> Warnings()
        {
            var isEntered = IsEntered();

            if (!unit.isDefined)
            {
                if (unit.definitionException != null)
                {
                    yield return Warning.Exception(unit.definitionException);
                }
                else if (!unit.canDefine)
                {
                    yield return Warning.Caution("Node is not properly configured.");
                }
            }
            else if (unit is MissingType)
            {
                var formerType = $"{(unit as MissingType)?.formerType}";
                formerType = string.IsNullOrEmpty(formerType) ? string.Empty : $"'{formerType}'";
                yield return new ActionButtonWarning(
                    WarningLevel.Error,
                    $"The source script for this node type can't be found. Did you remove its script?\n" +
                    $"Replace the node or add the {formerType} script file back to your project files.",
                    "Replace Node",
                    () =>
                    { UnitWidgetHelper.ReplaceUnit(unit, reference, context, context.selection, new EventWrapper(unit)); }
                );
                yield break;
            }

            if (!isEntered)
            {
                yield return Warning.Info("Node is never entered.");
            }

            // Obsolete attribute is not inherited, so traverse the chain manually
            var obsoleteAttribute = unit.GetType().AndHierarchy().FirstOrDefault(t => t.HasAttribute<ObsoleteAttribute>())?.GetAttribute<ObsoleteAttribute>();

            if (obsoleteAttribute != null)
            {
                var unitName = BoltFlowNameUtility.UnitTitle(unit.GetType(), true, false);

                if (obsoleteAttribute.Message != null)
                {
                    Debug.LogWarning($"\"{unitName}\" node is deprecated: {obsoleteAttribute.Message}");
                    yield return Warning.Caution($"Deprecated: {obsoleteAttribute.Message}");
                }
                else
                {
                    Debug.LogWarning($"\"{unitName}\" node is deprecated.");
                    yield return Warning.Caution("This node is deprecated.");
                }
            }

            if (unit.isDefined)
            {
                foreach (var invalidInput in unit.invalidInputs)
                {
                    yield return Warning.Caution($"{PortLabel(invalidInput)} is not used by this unit.");
                }

                foreach (var invalidOutput in unit.invalidOutputs)
                {
                    yield return Warning.Caution($"{PortLabel(invalidOutput)} is not provided by this unit.");
                }

                foreach (var validPort in unit.validPorts)
                {
                    if (validPort.hasInvalidConnection)
                    {
                        yield return Warning.Caution($"{PortLabel(validPort)} has an invalid connection.");
                    }
                }

#if UNITY_IOS || UNITY_ANDROID || UNITY_TVOS
                if (unit is IMouseEventUnit)
                {
                    var graphName = string.IsNullOrEmpty(unit.graph.title) ? "A ScriptGraph" : $"The ScriptGraph {unit.graph.title}";
                    var unitName = BoltFlowNameUtility.UnitTitle(unit.GetType(), true, false);
                    Debug.LogWarning($"{graphName} contains a {unitName} node. Presence of MouseEvent nodes might impact performance on handheld devices.");
                    yield return Warning.Caution("Presence of MouseEvent nodes might impact performance on handheld devices.");
                }
#endif
            }

            foreach (var controlInput in unit.controlInputs)
            {
                if (!controlInput.hasValidConnection)
                {
                    continue;
                }

                foreach (var relation in controlInput.relations)
                {
                    if (relation.source is ValueInput)
                    {
                        var valueInput = (ValueInput)relation.source;

                        foreach (var warning in ValueInputWarnings(valueInput))
                        {
                            yield return warning;
                        }
                    }
                }
            }

            foreach (var controlOutput in unit.controlOutputs)
            {
                if (!controlOutput.hasValidConnection)
                {
                    continue;
                }

                var controlInputs = controlOutput.relations.Select(r => r.source).OfType<ControlInput>();

                var isTriggered = !controlInputs.Any() || controlInputs.Any(ci => !ci.isPredictable || ci.couldBeEntered);

                foreach (var relation in controlOutput.relations)
                {
                    if (relation.source is ValueInput)
                    {
                        var valueInput = (ValueInput)relation.source;

                        foreach (var warning in ValueInputWarnings(valueInput))
                        {
                            yield return warning;
                        }
                    }
                }

                if (isEntered && !isTriggered)
                {
                    yield return Warning.Caution($"{PortLabel(controlOutput)} is connected, but it is never triggered.");
                }
            }

            foreach (var valueOutput in unit.valueOutputs)
            {
                if (!valueOutput.hasValidConnection)
                {
                    continue;
                }

                foreach (var relation in valueOutput.relations)
                {
                    if (relation.source is ControlInput)
                    {
                        var controlInput = (ControlInput)relation.source;

                        if (isEntered && controlInput.isPredictable && !controlInput.couldBeEntered)
                        {
                            yield return Warning.Severe($"{PortLabel(controlInput)} is required, but it is never entered.");
                        }
                    }
                    else if (relation.source is ValueInput)
                    {
                        var valueInput = (ValueInput)relation.source;

                        foreach (var warning in ValueInputWarnings(valueInput))
                        {
                            yield return warning;
                        }
                    }
                }
            }
        }

        private IEnumerable<Warning> ValueInputWarnings(ValueInput valueInput)
        {
            // We can disable null reference check if no self is available
            // and the port requires an owner, for example in macros.
            var trustFutureOwner = valueInput.nullMeansSelf && reference.self == null;

            var checkForNullReference = BoltFlow.Configuration.predictPotentialNullReferences && !valueInput.allowsNull && !trustFutureOwner;

            var checkForMissingComponent = BoltFlow.Configuration.predictPotentialMissingComponents && typeof(Component).IsAssignableFrom(valueInput.type);

            // Note that we cannot directly check the input's predicted value, because it
            // will return false for safeguard specifically because it might be missing requirements.
            // Therefore, we first check the connected value, then the default value.

            // If the port is connected to a predictable output, use the connected value to perform checks.
            if (valueInput.hasValidConnection)
            {
                var valueOutput = valueInput.validConnectedPorts.Single();

                if (Flow.CanPredict(valueOutput, reference))
                {
                    if (checkForNullReference)
                    {
                        if (Flow.Predict(valueOutput, reference) == null)
                        {
                            yield return Warning.Severe($"{PortLabel(valueInput)} cannot be null.");
                        }
                    }

                    if (checkForMissingComponent)
                    {
                        var connectedPredictedValue = Flow.Predict(valueOutput, reference);

                        // This check is necessary, because the predicted value could be
                        // incompatible as connections with non-guaranteed conversions are allowed.
                        if (ConversionUtility.CanConvert(connectedPredictedValue, typeof(GameObject), true))
                        {
                            var gameObject = ConversionUtility.Convert<GameObject>(connectedPredictedValue);

                            if (gameObject != null)
                            {
                                var component = (Component)ConversionUtility.Convert(gameObject, valueInput.type);

                                if (component == null)
                                {
                                    yield return Warning.Caution($"{PortLabel(valueInput)} is missing a {valueInput.type.DisplayName()} component.");
                                }
                            }
                        }
                    }
                }
            }
            // If the port isn't connected but has a default value, use the default value to perform checks.
            else if (valueInput.hasDefaultValue)
            {
                if (checkForNullReference)
                {
                    if (Flow.Predict(valueInput, reference) == null)
                    {
                        yield return Warning.Severe($"{PortLabel(valueInput)} cannot be null.");
                    }
                }

                if (checkForMissingComponent)
                {
                    var unconnectedPredictedValue = Flow.Predict(valueInput, reference);

                    if (ConversionUtility.CanConvert(unconnectedPredictedValue, typeof(GameObject), true))
                    {
                        var gameObject = ConversionUtility.Convert<GameObject>(unconnectedPredictedValue);

                        if (gameObject != null)
                        {
                            var component = (Component)ConversionUtility.Convert(gameObject, valueInput.type);

                            if (component == null)
                            {
                                yield return Warning.Caution($"{PortLabel(valueInput)} is missing a {valueInput.type.DisplayName()} component.");
                            }
                        }
                    }
                }
            }
            // The value isn't connected and has no default value,
            // therefore it is certain to be missing at runtime.
            else
            {
                yield return Warning.Severe($"{PortLabel(valueInput)} is missing.");
            }
        }
    }
}