A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using UnityEngine;
5
6namespace Unity.VisualScripting
7{
8 [Analyser(typeof(IUnit))]
9 public class UnitAnalyser<TUnit> : Analyser<TUnit, UnitAnalysis>
10 where TUnit : class, IUnit
11 {
12 public UnitAnalyser(GraphReference reference, TUnit target) : base(reference, target) { }
13
14 public TUnit unit => target;
15
16 [Assigns]
17 protected bool IsEntered()
18 {
19 using (var recursion = Recursion.New(1))
20 {
21 return IsEntered(unit, recursion);
22 }
23 }
24
25 private static bool IsEntered(IUnit unit, Recursion recursion)
26 {
27 if (unit.isControlRoot)
28 {
29 return true;
30 }
31
32 foreach (var controlInput in unit.controlInputs)
33 {
34 if (!controlInput.isPredictable || controlInput.couldBeEntered)
35 {
36 return true;
37 }
38 }
39
40 foreach (var valueOutput in unit.valueOutputs)
41 {
42 if (!recursion?.TryEnter(valueOutput) ?? false)
43 {
44 continue;
45 }
46
47 var valueOutputEntered = valueOutput.validConnections.Any(c => IsEntered(c.destination.unit, recursion));
48
49 recursion?.Exit(valueOutput);
50
51 if (valueOutputEntered)
52 {
53 return true;
54 }
55 }
56
57 return false;
58 }
59
60 private string PortLabel(IUnitPort port)
61 {
62 return port.Description<UnitPortDescription>().label;
63 }
64
65 [Assigns]
66 protected virtual IEnumerable<Warning> Warnings()
67 {
68 var isEntered = IsEntered();
69
70 if (!unit.isDefined)
71 {
72 if (unit.definitionException != null)
73 {
74 yield return Warning.Exception(unit.definitionException);
75 }
76 else if (!unit.canDefine)
77 {
78 yield return Warning.Caution("Node is not properly configured.");
79 }
80 }
81 else if (unit is MissingType)
82 {
83 var formerType = $"{(unit as MissingType)?.formerType}";
84 formerType = string.IsNullOrEmpty(formerType) ? string.Empty : $"'{formerType}'";
85 yield return new ActionButtonWarning(
86 WarningLevel.Error,
87 $"The source script for this node type can't be found. Did you remove its script?\n" +
88 $"Replace the node or add the {formerType} script file back to your project files.",
89 "Replace Node",
90 () =>
91 { UnitWidgetHelper.ReplaceUnit(unit, reference, context, context.selection, new EventWrapper(unit)); }
92 );
93 yield break;
94 }
95
96 if (!isEntered)
97 {
98 yield return Warning.Info("Node is never entered.");
99 }
100
101 // Obsolete attribute is not inherited, so traverse the chain manually
102 var obsoleteAttribute = unit.GetType().AndHierarchy().FirstOrDefault(t => t.HasAttribute<ObsoleteAttribute>())?.GetAttribute<ObsoleteAttribute>();
103
104 if (obsoleteAttribute != null)
105 {
106 var unitName = BoltFlowNameUtility.UnitTitle(unit.GetType(), true, false);
107
108 if (obsoleteAttribute.Message != null)
109 {
110 Debug.LogWarning($"\"{unitName}\" node is deprecated: {obsoleteAttribute.Message}");
111 yield return Warning.Caution($"Deprecated: {obsoleteAttribute.Message}");
112 }
113 else
114 {
115 Debug.LogWarning($"\"{unitName}\" node is deprecated.");
116 yield return Warning.Caution("This node is deprecated.");
117 }
118 }
119
120 if (unit.isDefined)
121 {
122 foreach (var invalidInput in unit.invalidInputs)
123 {
124 yield return Warning.Caution($"{PortLabel(invalidInput)} is not used by this unit.");
125 }
126
127 foreach (var invalidOutput in unit.invalidOutputs)
128 {
129 yield return Warning.Caution($"{PortLabel(invalidOutput)} is not provided by this unit.");
130 }
131
132 foreach (var validPort in unit.validPorts)
133 {
134 if (validPort.hasInvalidConnection)
135 {
136 yield return Warning.Caution($"{PortLabel(validPort)} has an invalid connection.");
137 }
138 }
139
140#if UNITY_IOS || UNITY_ANDROID || UNITY_TVOS
141 if (unit is IMouseEventUnit)
142 {
143 var graphName = string.IsNullOrEmpty(unit.graph.title) ? "A ScriptGraph" : $"The ScriptGraph {unit.graph.title}";
144 var unitName = BoltFlowNameUtility.UnitTitle(unit.GetType(), true, false);
145 Debug.LogWarning($"{graphName} contains a {unitName} node. Presence of MouseEvent nodes might impact performance on handheld devices.");
146 yield return Warning.Caution("Presence of MouseEvent nodes might impact performance on handheld devices.");
147 }
148#endif
149 }
150
151 foreach (var controlInput in unit.controlInputs)
152 {
153 if (!controlInput.hasValidConnection)
154 {
155 continue;
156 }
157
158 foreach (var relation in controlInput.relations)
159 {
160 if (relation.source is ValueInput)
161 {
162 var valueInput = (ValueInput)relation.source;
163
164 foreach (var warning in ValueInputWarnings(valueInput))
165 {
166 yield return warning;
167 }
168 }
169 }
170 }
171
172 foreach (var controlOutput in unit.controlOutputs)
173 {
174 if (!controlOutput.hasValidConnection)
175 {
176 continue;
177 }
178
179 var controlInputs = controlOutput.relations.Select(r => r.source).OfType<ControlInput>();
180
181 var isTriggered = !controlInputs.Any() || controlInputs.Any(ci => !ci.isPredictable || ci.couldBeEntered);
182
183 foreach (var relation in controlOutput.relations)
184 {
185 if (relation.source is ValueInput)
186 {
187 var valueInput = (ValueInput)relation.source;
188
189 foreach (var warning in ValueInputWarnings(valueInput))
190 {
191 yield return warning;
192 }
193 }
194 }
195
196 if (isEntered && !isTriggered)
197 {
198 yield return Warning.Caution($"{PortLabel(controlOutput)} is connected, but it is never triggered.");
199 }
200 }
201
202 foreach (var valueOutput in unit.valueOutputs)
203 {
204 if (!valueOutput.hasValidConnection)
205 {
206 continue;
207 }
208
209 foreach (var relation in valueOutput.relations)
210 {
211 if (relation.source is ControlInput)
212 {
213 var controlInput = (ControlInput)relation.source;
214
215 if (isEntered && controlInput.isPredictable && !controlInput.couldBeEntered)
216 {
217 yield return Warning.Severe($"{PortLabel(controlInput)} is required, but it is never entered.");
218 }
219 }
220 else if (relation.source is ValueInput)
221 {
222 var valueInput = (ValueInput)relation.source;
223
224 foreach (var warning in ValueInputWarnings(valueInput))
225 {
226 yield return warning;
227 }
228 }
229 }
230 }
231 }
232
233 private IEnumerable<Warning> ValueInputWarnings(ValueInput valueInput)
234 {
235 // We can disable null reference check if no self is available
236 // and the port requires an owner, for example in macros.
237 var trustFutureOwner = valueInput.nullMeansSelf && reference.self == null;
238
239 var checkForNullReference = BoltFlow.Configuration.predictPotentialNullReferences && !valueInput.allowsNull && !trustFutureOwner;
240
241 var checkForMissingComponent = BoltFlow.Configuration.predictPotentialMissingComponents && typeof(Component).IsAssignableFrom(valueInput.type);
242
243 // Note that we cannot directly check the input's predicted value, because it
244 // will return false for safeguard specifically because it might be missing requirements.
245 // Therefore, we first check the connected value, then the default value.
246
247 // If the port is connected to a predictable output, use the connected value to perform checks.
248 if (valueInput.hasValidConnection)
249 {
250 var valueOutput = valueInput.validConnectedPorts.Single();
251
252 if (Flow.CanPredict(valueOutput, reference))
253 {
254 if (checkForNullReference)
255 {
256 if (Flow.Predict(valueOutput, reference) == null)
257 {
258 yield return Warning.Severe($"{PortLabel(valueInput)} cannot be null.");
259 }
260 }
261
262 if (checkForMissingComponent)
263 {
264 var connectedPredictedValue = Flow.Predict(valueOutput, reference);
265
266 // This check is necessary, because the predicted value could be
267 // incompatible as connections with non-guaranteed conversions are allowed.
268 if (ConversionUtility.CanConvert(connectedPredictedValue, typeof(GameObject), true))
269 {
270 var gameObject = ConversionUtility.Convert<GameObject>(connectedPredictedValue);
271
272 if (gameObject != null)
273 {
274 var component = (Component)ConversionUtility.Convert(gameObject, valueInput.type);
275
276 if (component == null)
277 {
278 yield return Warning.Caution($"{PortLabel(valueInput)} is missing a {valueInput.type.DisplayName()} component.");
279 }
280 }
281 }
282 }
283 }
284 }
285 // If the port isn't connected but has a default value, use the default value to perform checks.
286 else if (valueInput.hasDefaultValue)
287 {
288 if (checkForNullReference)
289 {
290 if (Flow.Predict(valueInput, reference) == null)
291 {
292 yield return Warning.Severe($"{PortLabel(valueInput)} cannot be null.");
293 }
294 }
295
296 if (checkForMissingComponent)
297 {
298 var unconnectedPredictedValue = Flow.Predict(valueInput, reference);
299
300 if (ConversionUtility.CanConvert(unconnectedPredictedValue, typeof(GameObject), true))
301 {
302 var gameObject = ConversionUtility.Convert<GameObject>(unconnectedPredictedValue);
303
304 if (gameObject != null)
305 {
306 var component = (Component)ConversionUtility.Convert(gameObject, valueInput.type);
307
308 if (component == null)
309 {
310 yield return Warning.Caution($"{PortLabel(valueInput)} is missing a {valueInput.type.DisplayName()} component.");
311 }
312 }
313 }
314 }
315 }
316 // The value isn't connected and has no default value,
317 // therefore it is certain to be missing at runtime.
318 else
319 {
320 yield return Warning.Severe($"{PortLabel(valueInput)} is missing.");
321 }
322 }
323 }
324}