using System; using System.Collections.Generic; using System.Linq; namespace Unity.VisualScripting { /// /// Invokes a method or a constructor via reflection. /// public sealed class InvokeMember : MemberUnit { public InvokeMember() : base() { } public InvokeMember(Member member) : base(member) { } private bool useExpandedParameters; /// /// Whether the target should be output to allow for chaining. /// [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 inputParameters { get; private set; } /// /// The target object used when setting the value. /// [DoNotSerialize] [PortLabel("Target")] [PortLabelHidden] public ValueOutput targetOutput { get; private set; } [DoNotSerialize] [PortLabelHidden] public ValueOutput result { get; private set; } [DoNotSerialize] public Dictionary outputParameters { get; private set; } [DoNotSerialize] [PortLabelHidden] public ControlOutput exit { get; private set; } [DoNotSerialize] private int parameterCount; [Serialize] List 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(); outputParameters = new Dictionary(); 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 } }