暂无描述
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

UnitAnalyser.cs 12KB

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