A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections.Generic; 3using System.Linq; 4 5namespace Unity.VisualScripting 6{ 7 /// <summary> 8 /// Invokes a method or a constructor via reflection. 9 /// </summary> 10 public sealed class InvokeMember : MemberUnit 11 { 12 public InvokeMember() : base() { } 13 14 public InvokeMember(Member member) : base(member) { } 15 16 private bool useExpandedParameters; 17 18 /// <summary> 19 /// Whether the target should be output to allow for chaining. 20 /// </summary> 21 [Serialize] 22 [InspectableIf(nameof(supportsChaining))] 23 public bool chainable { get; set; } 24 25 [DoNotSerialize] 26 public bool supportsChaining => member.requiresTarget; 27 28 [DoNotSerialize] 29 [MemberFilter(Methods = true, Constructors = true)] 30 public Member invocation 31 { 32 get { return member; } 33 set { member = value; } 34 } 35 36 [DoNotSerialize] 37 [PortLabelHidden] 38 public ControlInput enter { get; private set; } 39 40 [DoNotSerialize] 41 public Dictionary<int, ValueInput> inputParameters { get; private set; } 42 43 /// <summary> 44 /// The target object used when setting the value. 45 /// </summary> 46 [DoNotSerialize] 47 [PortLabel("Target")] 48 [PortLabelHidden] 49 public ValueOutput targetOutput { get; private set; } 50 51 [DoNotSerialize] 52 [PortLabelHidden] 53 public ValueOutput result { get; private set; } 54 55 [DoNotSerialize] 56 public Dictionary<int, ValueOutput> outputParameters { get; private set; } 57 58 [DoNotSerialize] 59 [PortLabelHidden] 60 public ControlOutput exit { get; private set; } 61 62 [DoNotSerialize] 63 private int parameterCount; 64 65 [Serialize] 66 List<string> parameterNames; 67 68 public override bool HandleDependencies() 69 { 70 if (!base.HandleDependencies()) 71 return false; 72 73 // Here we have a chance to do a bit of post processing after deserialization of this node has occured. 74 75 // In the past we did not serialize parameter names explicitly (only parameter types), however, if we have 76 // exactly the same number of defaults as parameters, we happen to know what the original parameter names were. 77 // Note there is one specific exception that must be handled carefully, the base class (MemberUnit) adds a 78 // default value for the "target" (aka. the "this" instance) of the invocation; this does not correspond to 79 // a real parameter member so it is excluded here when trying to reconstruct the missing parameter names. 80 if (parameterNames == null && member.parameterTypes.Length == defaultValues.Count(d => d.Key != nameof(target))) 81 { 82 // Note that we strip the "%" prefix from the parameter name in the default values (the "%" denotes that 83 // it is a parameter input) 84 parameterNames = defaultValues 85 .Where(d => d.Key != nameof(target)) 86 .Select(defaultValue => defaultValue.Key.Substring(1)) 87 .ToList(); 88 } 89 90 return true; 91 } 92 93 protected override void Definition() 94 { 95 base.Definition(); 96 97 inputParameters = new Dictionary<int, ValueInput>(); 98 outputParameters = new Dictionary<int, ValueOutput>(); 99 useExpandedParameters = true; 100 101 enter = ControlInput(nameof(enter), Enter); 102 exit = ControlOutput(nameof(exit)); 103 Succession(enter, exit); 104 105 if (member.requiresTarget) 106 { 107 Requirement(target, enter); 108 } 109 110 if (supportsChaining && chainable) 111 { 112 targetOutput = ValueOutput(member.targetType, nameof(targetOutput)); 113 Assignment(enter, targetOutput); 114 } 115 116 if (member.isGettable) 117 { 118 result = ValueOutput(member.type, nameof(result), Result); 119 120 if (member.requiresTarget) 121 { 122 Requirement(target, result); 123 } 124 } 125 126 var parameterInfos = member.GetParameterInfos().ToArray(); 127 128 parameterCount = parameterInfos.Length; 129 130 bool needsParameterRemapping = false; 131 for (int parameterIndex = 0; parameterIndex < parameterCount; parameterIndex++) 132 { 133 var parameterInfo = parameterInfos[parameterIndex]; 134 135 var parameterType = parameterInfo.UnderlyingParameterType(); 136 137 if (!parameterInfo.HasOutModifier()) 138 { 139 var inputParameterKey = "%" + parameterInfo.Name; 140 141 // Changes in parameter names are tolerated, use the old parameter naming for now and fix it later. 142 if (parameterNames != null && parameterNames[parameterIndex] != parameterInfo.Name) 143 { 144 inputParameterKey = "%" + parameterNames[parameterIndex]; 145 needsParameterRemapping = true; 146 } 147 148 var inputParameter = ValueInput(parameterType, inputParameterKey); 149 150 inputParameters.Add(parameterIndex, inputParameter); 151 152 inputParameter.SetDefaultValue(parameterInfo.PseudoDefaultValue()); 153 154 if (parameterInfo.AllowsNull()) 155 { 156 inputParameter.AllowsNull(); 157 } 158 159 Requirement(inputParameter, enter); 160 161 if (member.isGettable) 162 { 163 Requirement(inputParameter, result); 164 } 165 } 166 167 if (parameterInfo.ParameterType.IsByRef || parameterInfo.IsOut) 168 { 169 var outputParameterKey = "&" + parameterInfo.Name; 170 171 // Changes in parameter names are tolerated, use the old parameter naming for now and fix it later. 172 if (parameterNames != null && parameterNames[parameterIndex] != parameterInfo.Name) 173 { 174 outputParameterKey = "&" + parameterNames[parameterIndex]; 175 needsParameterRemapping = true; 176 } 177 178 var outputParameter = ValueOutput(parameterType, outputParameterKey); 179 180 outputParameters.Add(parameterIndex, outputParameter); 181 182 Assignment(enter, outputParameter); 183 184 useExpandedParameters = false; 185 } 186 } 187 188 if (inputParameters.Count > 5) 189 { 190 useExpandedParameters = false; 191 } 192 193 if (parameterNames == null) 194 { 195 parameterNames = parameterInfos.Select(pInfo => pInfo.Name).ToList(); 196 } 197 198 if (needsParameterRemapping) 199 { 200 // Note, this will have no effect unless we are in an Editor context. This is okay since for runtime 201 // purposes as it is actually fine to continue to use the old parameter names for the sake of setting up 202 // connections and default values. The only reason it is interesting to update to the new parameter 203 // names is for UI purposes. 204 UnityThread.EditorAsync(PostDeserializeRemapParameterNames); 205 } 206 } 207 208 private void PostDeserializeRemapParameterNames() 209 { 210 var parameterInfos = member.GetParameterInfos().ToArray(); 211 212 // Sanity check 213 if (parameterNames?.Count != parameterInfos.Length) 214 return; 215 216 // Check if any of the method parameter names have changed (Note: handling of parameter type changes is not 217 // supported here, it is detected and handled elsewhere) 218 List<(ValueInput port, ValueOutput[] connectedSources)> renamedInputs = null; 219 List<(ValueOutput port, ValueInput[] connectedDestinations)> renamedOutputs = null; 220 List<(string name, object value)> renamedDefaults = null; 221 for (var i = 0; i < parameterInfos.Length; ++i) 222 { 223 var paramInfo = parameterInfos[i]; 224 var oldParamName = parameterNames[i]; 225 226 if (paramInfo.Name != oldParamName) 227 { 228 // Phase 1 of parameter renaming: disconnect any nodes connected to affected ports, remove affected 229 // ports from port definition, and remove any default values associated with affected ports. 230 if (valueInputs.TryGetValue("%" + oldParamName, out var oldInput)) 231 { 232 var connectionSources = oldInput.validConnections.Select(con => con.source).ToArray(); 233 foreach (var source in connectionSources) 234 source.DisconnectFromValid(oldInput); 235 236 valueInputs.Remove(oldInput); 237 238 if (renamedInputs == null) 239 renamedInputs = new List<(ValueInput, ValueOutput[])>(1); 240 renamedInputs.Add((new ValueInput("%" + paramInfo.Name, paramInfo.ParameterType), connectionSources)); 241 242 if (defaultValues.TryGetValue(oldInput.key, out var defaultValue)) 243 { 244 defaultValues.Remove(oldInput.key); 245 if (renamedDefaults == null) 246 renamedDefaults = new List<(string, object)>(1); 247 renamedDefaults.Add(("%" + paramInfo.Name, defaultValue)); 248 } 249 } 250 else if (valueOutputs.TryGetValue("&" + oldParamName, out var oldOutput)) 251 { 252 var connectionDestinations = oldOutput.validConnections.Select(con => con.destination).ToArray(); 253 foreach (var destination in connectionDestinations) 254 destination.DisconnectFromValid(oldOutput); 255 256 valueOutputs.Remove(oldOutput); 257 258 if (renamedOutputs == null) 259 renamedOutputs = new List<(ValueOutput, ValueInput[])>(1); 260 renamedOutputs.Add((new ValueOutput("&" + paramInfo.Name, paramInfo.ParameterType), connectionDestinations)); 261 } 262 263 parameterNames[i] = paramInfo.Name; 264 } 265 } 266 267 // Phase 2 of parameter renaming: add renamed version of affected ports back to the port definition, reconnect 268 // nodes back to those renamed ports, and redefine default values for those ports. 269 if (renamedInputs != null) 270 { 271 foreach (var renamedInput in renamedInputs) 272 { 273 valueInputs.Add(renamedInput.port); 274 foreach (var source in renamedInput.connectedSources) 275 source.ConnectToValid(renamedInput.port); 276 } 277 if (renamedDefaults != null) 278 { 279 foreach (var renamedDefault in renamedDefaults) 280 defaultValues[renamedDefault.name] = renamedDefault.value; 281 } 282 } 283 284 if (renamedOutputs != null) 285 { 286 foreach (var renamedOutput in renamedOutputs) 287 { 288 valueOutputs.Add(renamedOutput.port); 289 foreach (var destination in renamedOutput.connectedDestinations) 290 destination.ConnectToValid(renamedOutput.port); 291 } 292 } 293 294 295 if (renamedInputs != null || renamedOutputs != null) 296 { 297 Define(); 298 } 299 } 300 301 protected override bool IsMemberValid(Member member) 302 { 303 return member.isInvocable; 304 } 305 306 private object Invoke(object target, Flow flow) 307 { 308 if (useExpandedParameters) 309 { 310 switch (inputParameters.Count) 311 { 312 case 0: 313 314 return member.Invoke(target); 315 316 case 1: 317 318 return member.Invoke(target, 319 flow.GetConvertedValue(inputParameters[0])); 320 321 case 2: 322 323 return member.Invoke(target, 324 flow.GetConvertedValue(inputParameters[0]), 325 flow.GetConvertedValue(inputParameters[1])); 326 327 case 3: 328 329 return member.Invoke(target, 330 flow.GetConvertedValue(inputParameters[0]), 331 flow.GetConvertedValue(inputParameters[1]), 332 flow.GetConvertedValue(inputParameters[2])); 333 334 case 4: 335 336 return member.Invoke(target, 337 flow.GetConvertedValue(inputParameters[0]), 338 flow.GetConvertedValue(inputParameters[1]), 339 flow.GetConvertedValue(inputParameters[2]), 340 flow.GetConvertedValue(inputParameters[3])); 341 342 case 5: 343 344 return member.Invoke(target, 345 flow.GetConvertedValue(inputParameters[0]), 346 flow.GetConvertedValue(inputParameters[1]), 347 flow.GetConvertedValue(inputParameters[2]), 348 flow.GetConvertedValue(inputParameters[3]), 349 flow.GetConvertedValue(inputParameters[4])); 350 351 default: 352 353 throw new NotSupportedException(); 354 } 355 } 356 else 357 { 358 var arguments = new object[parameterCount]; 359 360 for (int parameterIndex = 0; parameterIndex < parameterCount; parameterIndex++) 361 { 362 if (inputParameters.TryGetValue(parameterIndex, out var inputParameter)) 363 { 364 arguments[parameterIndex] = flow.GetConvertedValue(inputParameter); 365 } 366 } 367 368 var result = member.Invoke(target, arguments); 369 370 for (int parameterIndex = 0; parameterIndex < parameterCount; parameterIndex++) 371 { 372 if (outputParameters.TryGetValue(parameterIndex, out var outputParameter)) 373 { 374 flow.SetValue(outputParameter, arguments[parameterIndex]); 375 } 376 } 377 378 return result; 379 } 380 } 381 382 private object GetAndChainTarget(Flow flow) 383 { 384 if (member.requiresTarget) 385 { 386 var target = flow.GetValue(this.target, member.targetType); 387 388 if (supportsChaining && chainable) 389 { 390 flow.SetValue(targetOutput, target); 391 } 392 393 return target; 394 } 395 396 return null; 397 } 398 399 private object Result(Flow flow) 400 { 401 var target = GetAndChainTarget(flow); 402 403 return Invoke(target, flow); 404 } 405 406 private ControlOutput Enter(Flow flow) 407 { 408 var target = GetAndChainTarget(flow); 409 410 var result = Invoke(target, flow); 411 412 if (this.result != null) 413 { 414 flow.SetValue(this.result, result); 415 } 416 417 return exit; 418 } 419 420 #region Analytics 421 422 public override AnalyticsIdentifier GetAnalyticsIdentifier() 423 { 424 const int maxNumParameters = 5; 425 var s = $"{member.targetType.FullName}.{member.name}"; 426 427 if (member.parameterTypes != null) 428 { 429 s += "("; 430 431 for (var i = 0; i < member.parameterTypes.Length; ++i) 432 { 433 if (i >= maxNumParameters) 434 { 435 s += $"->{i}"; 436 break; 437 } 438 439 s += member.parameterTypes[i].FullName; 440 if (i < member.parameterTypes.Length - 1) 441 s += ", "; 442 } 443 444 s += ")"; 445 } 446 447 var aid = new AnalyticsIdentifier 448 { 449 Identifier = s, 450 Namespace = member.targetType.Namespace 451 }; 452 aid.Hashcode = aid.Identifier.GetHashCode(); 453 return aid; 454 } 455 456 #endregion 457 } 458}