UnityGame/Library/PackageCache/com.unity.visualscripting/Runtime/VisualScripting.Flow/Framework/Codebase/InvokeMember.cs

459 lines
16 KiB
C#
Raw Normal View History

2024-10-27 10:53:47 +03:00
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
}
}