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}