459 lines
16 KiB
C#
459 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Unity.VisualScripting
|
|
{
|
|
/// <summary>
|
|
/// Invokes a method or a constructor via reflection.
|
|
/// </summary>
|
|
public sealed class InvokeMember : MemberUnit
|
|
{
|
|
public InvokeMember() : base() { }
|
|
|
|
public InvokeMember(Member member) : base(member) { }
|
|
|
|
private bool useExpandedParameters;
|
|
|
|
/// <summary>
|
|
/// Whether the target should be output to allow for chaining.
|
|
/// </summary>
|
|
[Serialize]
|
|
[InspectableIf(nameof(supportsChaining))]
|
|
public bool chainable { get; set; }
|
|
|
|
[DoNotSerialize]
|
|
public bool supportsChaining => member.requiresTarget;
|
|
|
|
[DoNotSerialize]
|
|
[MemberFilter(Methods = true, Constructors = true)]
|
|
public Member invocation
|
|
{
|
|
get { return member; }
|
|
set { member = value; }
|
|
}
|
|
|
|
[DoNotSerialize]
|
|
[PortLabelHidden]
|
|
public ControlInput enter { get; private set; }
|
|
|
|
[DoNotSerialize]
|
|
public Dictionary<int, ValueInput> inputParameters { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The target object used when setting the value.
|
|
/// </summary>
|
|
[DoNotSerialize]
|
|
[PortLabel("Target")]
|
|
[PortLabelHidden]
|
|
public ValueOutput targetOutput { get; private set; }
|
|
|
|
[DoNotSerialize]
|
|
[PortLabelHidden]
|
|
public ValueOutput result { get; private set; }
|
|
|
|
[DoNotSerialize]
|
|
public Dictionary<int, ValueOutput> outputParameters { get; private set; }
|
|
|
|
[DoNotSerialize]
|
|
[PortLabelHidden]
|
|
public ControlOutput exit { get; private set; }
|
|
|
|
[DoNotSerialize]
|
|
private int parameterCount;
|
|
|
|
[Serialize]
|
|
List<string> parameterNames;
|
|
|
|
public override bool HandleDependencies()
|
|
{
|
|
if (!base.HandleDependencies())
|
|
return false;
|
|
|
|
// Here we have a chance to do a bit of post processing after deserialization of this node has occured.
|
|
|
|
// In the past we did not serialize parameter names explicitly (only parameter types), however, if we have
|
|
// exactly the same number of defaults as parameters, we happen to know what the original parameter names were.
|
|
// Note there is one specific exception that must be handled carefully, the base class (MemberUnit) adds a
|
|
// default value for the "target" (aka. the "this" instance) of the invocation; this does not correspond to
|
|
// a real parameter member so it is excluded here when trying to reconstruct the missing parameter names.
|
|
if (parameterNames == null && member.parameterTypes.Length == defaultValues.Count(d => d.Key != nameof(target)))
|
|
{
|
|
// Note that we strip the "%" prefix from the parameter name in the default values (the "%" denotes that
|
|
// it is a parameter input)
|
|
parameterNames = defaultValues
|
|
.Where(d => d.Key != nameof(target))
|
|
.Select(defaultValue => defaultValue.Key.Substring(1))
|
|
.ToList();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override void Definition()
|
|
{
|
|
base.Definition();
|
|
|
|
inputParameters = new Dictionary<int, ValueInput>();
|
|
outputParameters = new Dictionary<int, ValueOutput>();
|
|
useExpandedParameters = true;
|
|
|
|
enter = ControlInput(nameof(enter), Enter);
|
|
exit = ControlOutput(nameof(exit));
|
|
Succession(enter, exit);
|
|
|
|
if (member.requiresTarget)
|
|
{
|
|
Requirement(target, enter);
|
|
}
|
|
|
|
if (supportsChaining && chainable)
|
|
{
|
|
targetOutput = ValueOutput(member.targetType, nameof(targetOutput));
|
|
Assignment(enter, targetOutput);
|
|
}
|
|
|
|
if (member.isGettable)
|
|
{
|
|
result = ValueOutput(member.type, nameof(result), Result);
|
|
|
|
if (member.requiresTarget)
|
|
{
|
|
Requirement(target, result);
|
|
}
|
|
}
|
|
|
|
var parameterInfos = member.GetParameterInfos().ToArray();
|
|
|
|
parameterCount = parameterInfos.Length;
|
|
|
|
bool needsParameterRemapping = false;
|
|
for (int parameterIndex = 0; parameterIndex < parameterCount; parameterIndex++)
|
|
{
|
|
var parameterInfo = parameterInfos[parameterIndex];
|
|
|
|
var parameterType = parameterInfo.UnderlyingParameterType();
|
|
|
|
if (!parameterInfo.HasOutModifier())
|
|
{
|
|
var inputParameterKey = "%" + parameterInfo.Name;
|
|
|
|
// Changes in parameter names are tolerated, use the old parameter naming for now and fix it later.
|
|
if (parameterNames != null && parameterNames[parameterIndex] != parameterInfo.Name)
|
|
{
|
|
inputParameterKey = "%" + parameterNames[parameterIndex];
|
|
needsParameterRemapping = true;
|
|
}
|
|
|
|
var inputParameter = ValueInput(parameterType, inputParameterKey);
|
|
|
|
inputParameters.Add(parameterIndex, inputParameter);
|
|
|
|
inputParameter.SetDefaultValue(parameterInfo.PseudoDefaultValue());
|
|
|
|
if (parameterInfo.AllowsNull())
|
|
{
|
|
inputParameter.AllowsNull();
|
|
}
|
|
|
|
Requirement(inputParameter, enter);
|
|
|
|
if (member.isGettable)
|
|
{
|
|
Requirement(inputParameter, result);
|
|
}
|
|
}
|
|
|
|
if (parameterInfo.ParameterType.IsByRef || parameterInfo.IsOut)
|
|
{
|
|
var outputParameterKey = "&" + parameterInfo.Name;
|
|
|
|
// Changes in parameter names are tolerated, use the old parameter naming for now and fix it later.
|
|
if (parameterNames != null && parameterNames[parameterIndex] != parameterInfo.Name)
|
|
{
|
|
outputParameterKey = "&" + parameterNames[parameterIndex];
|
|
needsParameterRemapping = true;
|
|
}
|
|
|
|
var outputParameter = ValueOutput(parameterType, outputParameterKey);
|
|
|
|
outputParameters.Add(parameterIndex, outputParameter);
|
|
|
|
Assignment(enter, outputParameter);
|
|
|
|
useExpandedParameters = false;
|
|
}
|
|
}
|
|
|
|
if (inputParameters.Count > 5)
|
|
{
|
|
useExpandedParameters = false;
|
|
}
|
|
|
|
if (parameterNames == null)
|
|
{
|
|
parameterNames = parameterInfos.Select(pInfo => pInfo.Name).ToList();
|
|
}
|
|
|
|
if (needsParameterRemapping)
|
|
{
|
|
// Note, this will have no effect unless we are in an Editor context. This is okay since for runtime
|
|
// purposes as it is actually fine to continue to use the old parameter names for the sake of setting up
|
|
// connections and default values. The only reason it is interesting to update to the new parameter
|
|
// names is for UI purposes.
|
|
UnityThread.EditorAsync(PostDeserializeRemapParameterNames);
|
|
}
|
|
}
|
|
|
|
private void PostDeserializeRemapParameterNames()
|
|
{
|
|
var parameterInfos = member.GetParameterInfos().ToArray();
|
|
|
|
// Sanity check
|
|
if (parameterNames?.Count != parameterInfos.Length)
|
|
return;
|
|
|
|
// Check if any of the method parameter names have changed (Note: handling of parameter type changes is not
|
|
// supported here, it is detected and handled elsewhere)
|
|
List<(ValueInput port, ValueOutput[] connectedSources)> renamedInputs = null;
|
|
List<(ValueOutput port, ValueInput[] connectedDestinations)> renamedOutputs = null;
|
|
List<(string name, object value)> renamedDefaults = null;
|
|
for (var i = 0; i < parameterInfos.Length; ++i)
|
|
{
|
|
var paramInfo = parameterInfos[i];
|
|
var oldParamName = parameterNames[i];
|
|
|
|
if (paramInfo.Name != oldParamName)
|
|
{
|
|
// Phase 1 of parameter renaming: disconnect any nodes connected to affected ports, remove affected
|
|
// ports from port definition, and remove any default values associated with affected ports.
|
|
if (valueInputs.TryGetValue("%" + oldParamName, out var oldInput))
|
|
{
|
|
var connectionSources = oldInput.validConnections.Select(con => con.source).ToArray();
|
|
foreach (var source in connectionSources)
|
|
source.DisconnectFromValid(oldInput);
|
|
|
|
valueInputs.Remove(oldInput);
|
|
|
|
if (renamedInputs == null)
|
|
renamedInputs = new List<(ValueInput, ValueOutput[])>(1);
|
|
renamedInputs.Add((new ValueInput("%" + paramInfo.Name, paramInfo.ParameterType), connectionSources));
|
|
|
|
if (defaultValues.TryGetValue(oldInput.key, out var defaultValue))
|
|
{
|
|
defaultValues.Remove(oldInput.key);
|
|
if (renamedDefaults == null)
|
|
renamedDefaults = new List<(string, object)>(1);
|
|
renamedDefaults.Add(("%" + paramInfo.Name, defaultValue));
|
|
}
|
|
}
|
|
else if (valueOutputs.TryGetValue("&" + oldParamName, out var oldOutput))
|
|
{
|
|
var connectionDestinations = oldOutput.validConnections.Select(con => con.destination).ToArray();
|
|
foreach (var destination in connectionDestinations)
|
|
destination.DisconnectFromValid(oldOutput);
|
|
|
|
valueOutputs.Remove(oldOutput);
|
|
|
|
if (renamedOutputs == null)
|
|
renamedOutputs = new List<(ValueOutput, ValueInput[])>(1);
|
|
renamedOutputs.Add((new ValueOutput("&" + paramInfo.Name, paramInfo.ParameterType), connectionDestinations));
|
|
}
|
|
|
|
parameterNames[i] = paramInfo.Name;
|
|
}
|
|
}
|
|
|
|
// Phase 2 of parameter renaming: add renamed version of affected ports back to the port definition, reconnect
|
|
// nodes back to those renamed ports, and redefine default values for those ports.
|
|
if (renamedInputs != null)
|
|
{
|
|
foreach (var renamedInput in renamedInputs)
|
|
{
|
|
valueInputs.Add(renamedInput.port);
|
|
foreach (var source in renamedInput.connectedSources)
|
|
source.ConnectToValid(renamedInput.port);
|
|
}
|
|
if (renamedDefaults != null)
|
|
{
|
|
foreach (var renamedDefault in renamedDefaults)
|
|
defaultValues[renamedDefault.name] = renamedDefault.value;
|
|
}
|
|
}
|
|
|
|
if (renamedOutputs != null)
|
|
{
|
|
foreach (var renamedOutput in renamedOutputs)
|
|
{
|
|
valueOutputs.Add(renamedOutput.port);
|
|
foreach (var destination in renamedOutput.connectedDestinations)
|
|
destination.ConnectToValid(renamedOutput.port);
|
|
}
|
|
}
|
|
|
|
|
|
if (renamedInputs != null || renamedOutputs != null)
|
|
{
|
|
Define();
|
|
}
|
|
}
|
|
|
|
protected override bool IsMemberValid(Member member)
|
|
{
|
|
return member.isInvocable;
|
|
}
|
|
|
|
private object Invoke(object target, Flow flow)
|
|
{
|
|
if (useExpandedParameters)
|
|
{
|
|
switch (inputParameters.Count)
|
|
{
|
|
case 0:
|
|
|
|
return member.Invoke(target);
|
|
|
|
case 1:
|
|
|
|
return member.Invoke(target,
|
|
flow.GetConvertedValue(inputParameters[0]));
|
|
|
|
case 2:
|
|
|
|
return member.Invoke(target,
|
|
flow.GetConvertedValue(inputParameters[0]),
|
|
flow.GetConvertedValue(inputParameters[1]));
|
|
|
|
case 3:
|
|
|
|
return member.Invoke(target,
|
|
flow.GetConvertedValue(inputParameters[0]),
|
|
flow.GetConvertedValue(inputParameters[1]),
|
|
flow.GetConvertedValue(inputParameters[2]));
|
|
|
|
case 4:
|
|
|
|
return member.Invoke(target,
|
|
flow.GetConvertedValue(inputParameters[0]),
|
|
flow.GetConvertedValue(inputParameters[1]),
|
|
flow.GetConvertedValue(inputParameters[2]),
|
|
flow.GetConvertedValue(inputParameters[3]));
|
|
|
|
case 5:
|
|
|
|
return member.Invoke(target,
|
|
flow.GetConvertedValue(inputParameters[0]),
|
|
flow.GetConvertedValue(inputParameters[1]),
|
|
flow.GetConvertedValue(inputParameters[2]),
|
|
flow.GetConvertedValue(inputParameters[3]),
|
|
flow.GetConvertedValue(inputParameters[4]));
|
|
|
|
default:
|
|
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var arguments = new object[parameterCount];
|
|
|
|
for (int parameterIndex = 0; parameterIndex < parameterCount; parameterIndex++)
|
|
{
|
|
if (inputParameters.TryGetValue(parameterIndex, out var inputParameter))
|
|
{
|
|
arguments[parameterIndex] = flow.GetConvertedValue(inputParameter);
|
|
}
|
|
}
|
|
|
|
var result = member.Invoke(target, arguments);
|
|
|
|
for (int parameterIndex = 0; parameterIndex < parameterCount; parameterIndex++)
|
|
{
|
|
if (outputParameters.TryGetValue(parameterIndex, out var outputParameter))
|
|
{
|
|
flow.SetValue(outputParameter, arguments[parameterIndex]);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private object GetAndChainTarget(Flow flow)
|
|
{
|
|
if (member.requiresTarget)
|
|
{
|
|
var target = flow.GetValue(this.target, member.targetType);
|
|
|
|
if (supportsChaining && chainable)
|
|
{
|
|
flow.SetValue(targetOutput, target);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private object Result(Flow flow)
|
|
{
|
|
var target = GetAndChainTarget(flow);
|
|
|
|
return Invoke(target, flow);
|
|
}
|
|
|
|
private ControlOutput Enter(Flow flow)
|
|
{
|
|
var target = GetAndChainTarget(flow);
|
|
|
|
var result = Invoke(target, flow);
|
|
|
|
if (this.result != null)
|
|
{
|
|
flow.SetValue(this.result, result);
|
|
}
|
|
|
|
return exit;
|
|
}
|
|
|
|
#region Analytics
|
|
|
|
public override AnalyticsIdentifier GetAnalyticsIdentifier()
|
|
{
|
|
const int maxNumParameters = 5;
|
|
var s = $"{member.targetType.FullName}.{member.name}";
|
|
|
|
if (member.parameterTypes != null)
|
|
{
|
|
s += "(";
|
|
|
|
for (var i = 0; i < member.parameterTypes.Length; ++i)
|
|
{
|
|
if (i >= maxNumParameters)
|
|
{
|
|
s += $"->{i}";
|
|
break;
|
|
}
|
|
|
|
s += member.parameterTypes[i].FullName;
|
|
if (i < member.parameterTypes.Length - 1)
|
|
s += ", ";
|
|
}
|
|
|
|
s += ")";
|
|
}
|
|
|
|
var aid = new AnalyticsIdentifier
|
|
{
|
|
Identifier = s,
|
|
Namespace = member.targetType.Namespace
|
|
};
|
|
aid.Hashcode = aid.Identifier.GetHashCode();
|
|
return aid;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|