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}