Search Unity

Parameters do not match signature with DynamicInvoke

Discussion in 'Scripting' started by Studio_Akiba, Jul 3, 2022.

  1. Studio_Akiba

    Studio_Akiba

    Joined:
    Mar 3, 2014
    Posts:
    1,426
    Hi everyone, I'm trying to build an internal command console and I've run into a weird error I've never seen before, I'm getting the following when trying to pass parameters via DynamicInvoke:
    Code (CSharp):
    1. TargetParameterCountException: parameters do not match signature
    The code benig used to call it looks something like this:
    Code (CSharp):
    1. string args = input.Remove(0, input.IndexOf(' ') + 1);
    2. libLv3.Key.DynamicInvoke(args);
    Essentially, I'm stripping out anything before the first space (the command) and applying the remainder as an argument to the method, which is stored as an Action.
    To allow the Action to reference a method with arguments, I'm using a parameterless lambda to prevent the "cannot convert from method group to action", which I think may be the issue.
    Code (CSharp):
    1. console.RegisterCommands("GiveHealth", "Gives player a set amount of health", () => GiveHealth(), true, false);
    2.  
    3. void GiveHealth(int amount = 0)
    4.     {
    5.         Arcane_GameManager.manager.AddHealth(amount);
    6.     }
    I can't seem to work out how to produce a generic solution to allow me to register commands, some of which can take arguments without the arguments simply not working. What exactly am I doing wrong here? Is it the lambda conversion preventing arguments being passed?
     
  2. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,304
    Looks like the args are supposed to be an array of objects, not a string
    https://www.thecodeteacher.com/question/29212/c

    That doesn't work. You'd need to include the arguments in that lambda call for it to work, but then that action will only ever be able to invoke with those arguments.
     
    Last edited: Jul 3, 2022
  3. Studio_Akiba

    Studio_Akiba

    Joined:
    Mar 3, 2014
    Posts:
    1,426
    I've reworked things to place the args in an array, but I'm still getting the same error:
    Code (CSharp):
    1. string args = input.Remove(0, input.IndexOf(' ') + 1);
    2.                                     string[] argArray = args.Split(' ');
    3.                                     libLv3.Key.DynamicInvoke(argArray);
     
  4. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,304
    Maybe this will shed some light

    Using reflection we get the type of the class we are in
    We fetch the function we are interested in
    We fetch the parameters of the function we are interested in
    We convert our string parameters to the appropriate parameter types
    We then add those converted parameters to an object list
    We invoke the method with the parameter object list as an array

    That works. If we don't convert those parameters to their proper types, it fails for both invoke and dynamic invoke.
    DynamicInvoke seemingly requires us to declare an action/func with those parameters?
    Invoke doesn't

    LocalMethodCall "CalculateAgeThisYear 2000"


    Code (CSharp):
    1.  
    2. using System;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using System.Reflection;
    6. using UnityEngine;
    7.  
    8. public class ReflectionMethod : MonoBehaviour
    9. {
    10.     public string LocalMethodCall;
    11.     private string functionname;
    12.     private List<object> parameters = new();
    13.     // Start is called before the first frame update
    14.     void Start()
    15.     {
    16.         Type thisType = this.GetType();
    17.  
    18.         var methodCallsplit = LocalMethodCall.Split(' ');
    19.         functionname = methodCallsplit[0];
    20.  
    21.         MethodInfo theMethod = thisType.GetMethod(functionname);
    22.  
    23.         ParameterInfo[] paraminfo = theMethod.GetParameters();
    24.  
    25.         for (int i = 1; i < methodCallsplit.Length; i++)
    26.         {
    27.             var param = Convert.ChangeType(methodCallsplit[i], paraminfo[i-1].ParameterType);
    28.             parameters.Add(param);
    29.  
    30.         }
    31.  
    32.         //invoke
    33.         theMethod.Invoke(this, parameters.ToArray());
    34.  
    35.         //dynamicinvoke
    36.         Action<int> act = CalculateAgeThisYear;
    37.         act.DynamicInvoke(parameters.ToArray());
    38.     }
    39.  
    40.     public void CalculateAgeThisYear(int yearOfBirth )
    41.     {
    42.         Debug.Log($"Age: {DateTime.Now.Year - yearOfBirth}");
    43.     }
    44. }
     
  5. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,304
    I did find out how to create a delegate at runtime with variable parameters for the dynamicinvoke.
    Haven't looked into funcs and returns or anything

    Code (CSharp):
    1.  
    2. using System;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using System.Reflection;
    6. using UnityEngine;
    7.  
    8. public class ReflectionMethod : MonoBehaviour
    9. {
    10.     private string LocalMethodCall1 = "CalculateAgeThisYear 2000";
    11.     private string LocalMethodCall2 = "Test";
    12.  
    13.     private delegate void Del();
    14.  
    15.     // Start is called before the first frame update
    16.     void Start()
    17.     {
    18.         ReflectionInvoke(LocalMethodCall1);
    19.         ReflectionInvoke(LocalMethodCall2);
    20.  
    21.         //dynamicinvoke
    22.         //Action<int> act = CalculateAgeThisYear;
    23.         //act.DynamicInvoke(parameters.ToArray());
    24.     }
    25.  
    26.     public void ReflectionInvoke(string methodCall)
    27.     {
    28.         string functionname;
    29.         List<object> parameters = new();
    30.  
    31.         Type thisType = this.GetType();
    32.  
    33.         var methodCallsplit = methodCall.Split(' ');
    34.         functionname = methodCallsplit[0];
    35.  
    36.         MethodInfo theMethod = thisType.GetMethod(functionname);
    37.  
    38.         ParameterInfo[] paraminfo = theMethod.GetParameters();
    39.         List<System.Type> paramTypes = new();
    40.  
    41.         for (int i = 1; i < methodCallsplit.Length; i++)
    42.         {
    43.             paramTypes.Add(paraminfo[i - 1].ParameterType);
    44.             var param = Convert.ChangeType(methodCallsplit[i], paraminfo[i - 1].ParameterType);
    45.             parameters.Add(param);
    46.  
    47.         }
    48.  
    49.         //Invoke
    50.         theMethod.Invoke(this, parameters.ToArray());
    51.  
    52.         //Dynamic Invoke
    53.         var actionType = System.Linq.Expressions.Expression.GetActionType(paramTypes.ToArray());
    54.         Delegate act = theMethod.CreateDelegate(actionType, this);
    55.         act.DynamicInvoke(parameters.ToArray());
    56.     }
    57.  
    58.     public void CalculateAgeThisYear(int yearOfBirth )
    59.     {
    60.         Debug.Log($"Age: {DateTime.Now.Year - yearOfBirth}");
    61.     }
    62.  
    63.     public void Test()
    64.     {
    65.         Debug.Log("Ran Parameterless Test");
    66.     }
    67.  
    68. }
     
  6. Studio_Akiba

    Studio_Akiba

    Joined:
    Mar 3, 2014
    Posts:
    1,426
    This seems like more what I'm after, but you seem to be using MethodInfo to create a delegate, how do you do that? Your code just gives me a "MethodInfo does not contain a definition for CreateDelegate" error.
     
  7. Studio_Akiba

    Studio_Akiba

    Joined:
    Mar 3, 2014
    Posts:
    1,426
    Ok, so I think I'm getting somewhere with this FINALLY, but keep hitting a wall.

    Here is the code:

    Code (CSharp):
    1. List<object> parameters = new List<object>();
    2.  
    3.                                     string args = input.Remove(0, input.IndexOf(' ') + 1);
    4.                                     string[] argArray = args.Split(' ');
    5.                                     PrintLn("");
    6.  
    7.                                     MethodInfo mi = libLv3.Key.Method;
    8.                                     ParameterInfo[] pInfo = mi.GetParameters();
    9.                                     List<Type> paramTypes = new List<Type>();
    10.  
    11.                                     for (int i = 1; i < callSplit.Length; i++)
    12.                                     {
    13.                                         Debug.Log(callSplit[i]);
    14.                                         Debug.Log(pInfo[i]);
    15.                                         object param = Convert.ChangeType(callSplit[i], pInfo[0].ParameterType);
    16.                                         parameters.Add(param);
    17.                                     }
    18.  
    19.                                     mi.Invoke(this, parameters.ToArray());
    New issue, I've tried to make sure the same method can still be used to register both arg and non-arg methods, so I've used a lambda to enable this (hence the MethodInfo stuff), but it simply can't see the parameter, the pInfo line always returns null.

    Code (CSharp):
    1. void AddHealth(int amount = 0)
    2.     {
    3.         Arcane_GameManager.manager.AddHealth(amount);
    4.     }
    Code (CSharp):
    1. c.RegisterCommands("SetHealth", "Adds a specified amount of health to the player", () => AddHealth(), false, false);
    Am I just on a fool's errand, is this even possible to do properly?
     
  8. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,011
    What you're doing here does not work at all. Your RegisterCommand seems to expect a parameterless action. Therefore you can not pass any arguments to the methods you register here. You create a wrapper lambda expression like this:

    Code (CSharp):
    1. () => AddHealth()
    That means you create a new anonymous method that doesn't have any arguments at all. inside the body of that method youc all your AddHealth method. Since you don't pass any arguments to your AddHealth method, your default argument of "0" applies here. So your lambda expression is equivalent to this:

    Code (CSharp):
    1. void AnonymousMethod()
    2. {
    3.     AddHealth(0);
    4. }
    So you register a delegate to this "AnonymousMethod". You can't pass arguments to a delegate that does not have any arguments. To me it's still not really clear what you want to do. DynamicInvoke doesn't allow you do change or omit arguments. All it does is late bind the method and arguments. However you still have to provide the exact same number of arguments with the exact types.

    If this is about some kind of console commands, you probably want to change the delegate type to something that takes a params string array. That way each method could interpret / convert the arguments as they need. You can not store different delegate types into a single delegate type. C# is a strongly typed language. The most flexible approach is to use an object array which allows all sorts of arguments, however the method has to check, interpret, convert the arguments itself.

    Of course you could build some kind of mediating layer that helps to manually convert the arguments as you create the lambda expression.

    So I would suggest to first change the delegate type of your command system to something like

    Code (CSharp):
    1. public delegate void UserCommand(params string[] aArgs);
    That way the calling system is free to pass any number of string arguments to the registered methods. All those methods would receive a string array with the potential variable arguments.

    As a second step you may want to write a few extension methods that would help you to manually convert, parse and handle the arguments when you create your lambda expression. Here are a few examples:

    Code (CSharp):
    1.     public static class ArgumentArrayExtensions
    2.     {
    3.         public static float ArgFloat(this string[] aArgs, int aIndex)
    4.         {
    5.             if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    6.                 throw new System.Exception("Wrong argument count");
    7.             if (float.TryParse(aArgs[aIndex], out float value))
    8.                 return value;
    9.             throw new System.Exception("Passed argument can't be parsed as float");
    10.         }
    11.         public static int ArgInt(this string[] aArgs, int aIndex)
    12.         {
    13.             if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    14.                 throw new System.Exception("Wrong argument count");
    15.             if (int.TryParse(aArgs[aIndex], out int value))
    16.                 return value;
    17.             throw new System.Exception("Passed argument can't be parsed as float");
    18.         }
    19.         public static float ArgFloatDefault(this string[] aArgs, int aIndex, float aDefault = 0f)
    20.         {
    21.             if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    22.                 return aDefault;
    23.             if (float.TryParse(aArgs[aIndex], out float value))
    24.                 return value;
    25.             return aDefault;
    26.         }
    27.         public static int ArgIntDefault(this string[] aArgs, int aIndex, int aDefault = 0)
    28.         {
    29.             if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    30.                 return aDefault;
    31.             if (int.TryParse(aArgs[aIndex], out int value))
    32.                 return value;
    33.             return aDefault;
    34.         }
    35.  
    36.     }
    37.  
    Those extension methods for a string array would allow you to grab and convert a certain argument to the desired type. The first two variants would throw an error when something's not right, the last two examples allows you to specify a default value that is used in case the argument is not provided or the argument could not be converted into the desired type.

    When you register your "AddHealth" method you could do this:

    Code (CSharp):
    1. RegisterCommands(
    2.     "SetHealth",
    3.     "Adds a specified amount of health to the player",
    4.     args => Arcane_GameManager.manager.AddHealth(args.ArgIntDefault(0, 5)),
    5.     false, false
    6. );
    In this case "RegisterCommands" (which should be called RegisterCommand) would take a UserCommand delegate. The "args" is our string array. So every command would get this string array. We use the ArgIntDefault extension method to get the value at index 0 and convert it into an integer and pass it to the actual method we want to call. If something is wrong (no argument or the first argument can not be converted into an integer) the ArgIntDefault method would return a value of 5, so we can savely call the AddHealth method because we have to pass an int argument.

    Of course you could add more such utility methods to get other arguments like Vector3 or whatever you may also need for your commands.
     
    Last edited: Dec 8, 2022
  9. Studio_Akiba

    Studio_Akiba

    Joined:
    Mar 3, 2014
    Posts:
    1,426
    What I have is a sort of command line in-game, where you can type in a command which is then compared to a dictionary containing public string names (what you type in), and Actions (what actually gets called).

    What I'm trying to do is have some of them require arguments, where after typing in the command you need to put in a space then additional information, so:
    Code (CSharp):
    1. AddHealth 10
    would call that AddHealth method and pass the 10 string, parsed into an int, as the argument.

    I've tried a variety of things for this, but can't get it working in a generic way.
     
  10. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    Put a breaker. Read the console text input as a string; AddHealth; 10 I like to use the ‘;’ as a breaker. But you could use a -, or else ask for the white space. Extract your command from the string. It should be everything before the white space, while everything else but not including the white space may represent the amount of health you will go on to add.

    Here’s your character is digit checker
    https://learn.microsoft.com/en-us/dotnet/api/system.char.isdigit?view=net-7.0

    and here’s your character is white space
    https://learn.microsoft.com/en-us/dotnet/api/system.char.iswhitespace?view=net-7.0

    and so you’ll have to read the string and check for a numerical value or an extension to your methodname. Kind of like a valve console, and then separate the two.
     
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,011
    Uhm, are you sure you read my full answer? I suggest that every command has the above provided signature. I gave an example how you can then dynamically grab the arguments from the argument array. That's why I wrote those extention methods.

    edit

    Here's a complete example of a console command interpreter as well as a simple dynamic variable container.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. #region CommandHandler
    6. public delegate void UserCommand(params string[] aArgs);
    7.  
    8. public interface ICommand
    9. {
    10.     string Name { get; }
    11.     string Description { get; }
    12.     void Execute(params string[] aArgs);
    13. }
    14.  
    15. public class Command : ICommand
    16. {
    17.     public string Name { get; protected set; }
    18.     public string Description { get; protected set; }
    19.     protected UserCommand cmd;
    20.     public Command(string aName, string aDescription, UserCommand aCommand)
    21.     {
    22.         Name = aName;
    23.         Description = aDescription;
    24.         cmd = aCommand;
    25.     }
    26.     public virtual void Execute(params string[] aArgs)
    27.     {
    28.         if (cmd != null)
    29.             cmd(aArgs);
    30.     }
    31. }
    32.  
    33. public class CommandHandler
    34. {
    35.     Dictionary<string, ICommand> m_Commands = new Dictionary<string, ICommand>();
    36.     public void HandleInput(string aCommand)
    37.     {
    38.         if (string.IsNullOrEmpty(aCommand))
    39.             return;
    40.         string[] cmdLine = aCommand.Split(' ');
    41.         if (m_Commands.TryGetValue(cmdLine[0], out ICommand cmd))
    42.             cmd.Execute(cmdLine);
    43.     }
    44.     public CommandHandler RegisterCommand(string aCmdName, string aDescription, UserCommand aCmd)
    45.     {
    46.         return RegisterCommand(new Command(aCmdName, aDescription, aCmd));
    47.     }
    48.     public CommandHandler RegisterCommand(ICommand aCommand)
    49.     {
    50.         m_Commands.Add(aCommand.Name, aCommand);
    51.         return this;
    52.     }
    53. }
    54.  
    55. public static class ArgumentArrayExtensions
    56. {
    57.     public static float ArgFloat(this string[] aArgs, int aIndex, float aDefault = 0f)
    58.     {
    59.         if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    60.             return aDefault;
    61.         if (float.TryParse(aArgs[aIndex], out var value))
    62.             return value;
    63.         return aDefault;
    64.     }
    65.     public static int ArgInt(this string[] aArgs, int aIndex, int aDefault = 0)
    66.     {
    67.         if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    68.             return aDefault;
    69.         if (int.TryParse(aArgs[aIndex], out var value))
    70.             return value;
    71.         return aDefault;
    72.     }
    73.     public static bool ArgBool(this string[] aArgs, int aIndex, bool aDefault = false)
    74.     {
    75.         if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    76.             return aDefault;
    77.         if (bool.TryParse(aArgs[aIndex], out var value))
    78.             return value;
    79.         return aDefault;
    80.     }
    81.     public static string ArgString(this string[] aArgs, int aIndex, string aDefault = "")
    82.     {
    83.         if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    84.             return aDefault;
    85.         return aArgs[aIndex];
    86.     }
    87.     public static Vector3 ArgVector3(this string[] aArgs, int aIndex, Vector3 aDefault = default)
    88.     {
    89.         if (aArgs == null || aIndex < 0 || aIndex >= aArgs.Length)
    90.             return aDefault;
    91.         return new Vector3(aArgs.ArgFloat(aIndex), aArgs.ArgFloat(aIndex + 1), aArgs.ArgFloat(aIndex + 2));
    92.     }
    93. }
    94. #endregion CommandHandler
    95.  
    96. #region DynVars
    97. public class DynVarContainer
    98. {
    99.     public interface IDynVar
    100.     {
    101.         string Name { get; }
    102.         string Value { get; set; }
    103.     }
    104.  
    105.     public class DynVar : IDynVar
    106.     {
    107.         public string Name { get; protected set; }
    108.         public string Value { get; set; }
    109.         public DynVar(string aName, string aInitialValue = "")
    110.         {
    111.             Name = aName;
    112.             Value = aInitialValue;
    113.         }
    114.     }
    115.     public class DynVarWrapper : IDynVar
    116.     {
    117.         public string Name { get; protected set; }
    118.         protected System.Action<string> m_Setter;
    119.         protected System.Func<string> m_Getter;
    120.         public string Value { get => m_Getter(); set => m_Setter(value); }
    121.         public DynVarWrapper(string aName, System.Func<string> aGetter, System.Action<string> aSetter)
    122.         {
    123.             Name = aName;
    124.             m_Getter = aGetter;
    125.             m_Setter = aSetter;
    126.         }
    127.     }
    128.     public class DynVarFloat : DynVarWrapper
    129.     {
    130.         public DynVarFloat(string aName, System.Func<float> aGetter, System.Action<float> aSetter) :
    131.             base(aName, () => aGetter().ToString(), a => aSetter(float.TryParse(a, out var f) ? f : 0f))
    132.         { }
    133.     }
    134.     public class DynVarInt : DynVarWrapper
    135.     {
    136.         public DynVarInt(string aName, System.Func<int> aGetter, System.Action<int> aSetter) :
    137.             base(aName, () => aGetter().ToString(), a => aSetter(int.TryParse(a, out var i) ? i : 0))
    138.         { }
    139.     }
    140.     public class DynVarBool : DynVarWrapper
    141.     {
    142.         public DynVarBool(string aName, System.Func<bool> aGetter, System.Action<bool> aSetter) :
    143.             base(aName, () => aGetter().ToString(), a => aSetter(bool.TryParse(a, out var b) ? b : false))
    144.         { }
    145.     }
    146.  
    147.     protected Dictionary<string, IDynVar> m_Variables = new Dictionary<string, IDynVar>();
    148.     public string this[string aVarName]
    149.     {
    150.         get => m_Variables.TryGetValue(aVarName, out var v) ? v.Value : "";
    151.         set
    152.         {
    153.             if (m_Variables.TryGetValue(aVarName, out var v))
    154.                 v.Value = value;
    155.             else
    156.                 m_Variables.Add(aVarName, new DynVar(aVarName, value));
    157.         }
    158.     }
    159.     public void AddVar(IDynVar aVariable)
    160.     {
    161.         m_Variables.Add(aVariable.Name, aVariable);
    162.     }
    163.     public void AddFloatVar(string aName, System.Func<float> aGetter, System.Action<float> aSetter)
    164.     {
    165.         m_Variables.Add(aName, new DynVarFloat(aName, aGetter, aSetter));
    166.     }
    167.     public void AddIntVar(string aName, System.Func<int> aGetter, System.Action<int> aSetter)
    168.     {
    169.         m_Variables.Add(aName, new DynVarInt(aName, aGetter, aSetter));
    170.     }
    171.     public void AddBoolVar(string aName, System.Func<bool> aGetter, System.Action<bool> aSetter)
    172.     {
    173.         m_Variables.Add(aName, new DynVarBool(aName, aGetter, aSetter));
    174.     }
    175.  
    176.     public void RemoveVar(string aVarName)
    177.     {
    178.         if (m_Variables.TryGetValue(aVarName, out IDynVar v)&& v is DynVar)
    179.             m_Variables.Remove(aVarName);
    180.     }
    181. }
    182. #endregion CommandHandler
    183.  
    184.  
    185. public class Console : MonoBehaviour
    186. {
    187.     CommandHandler cmdHandler = new CommandHandler();
    188.     DynVarContainer varContainer = new DynVarContainer();
    189.     float hp = 100;
    190.     void Start()
    191.     {
    192.         cmdHandler.RegisterCommand("set", "Set a variable", a => varContainer[a.ArgString(1)] = a.ArgString(2));
    193.         cmdHandler.RegisterCommand("get", "Get a variable", a => WriteLine(a.ArgString(1) + " -> " + varContainer[a.ArgString(1)]));
    194.         cmdHandler.RegisterCommand("tp", "teleport player to position", a=>Teleport(a.ArgString(1),a.ArgVector3(2)));
    195.  
    196.         varContainer.AddFloatVar("hp", ()=>hp, a=>hp=a);
    197.  
    198.  
    199.         // example inputs
    200.         cmdHandler.HandleInput("get hp");
    201.         cmdHandler.HandleInput("set hp 2000");
    202.         cmdHandler.HandleInput("get hp");
    203.  
    204.         cmdHandler.HandleInput("get newVar");
    205.  
    206.         cmdHandler.HandleInput("set newVar test");
    207.         cmdHandler.HandleInput("get newVar");
    208.         Debug.Log("newVar = " + varContainer["newVar"]);
    209.     }
    210.  
    211.     void Teleport(string aPlayerName, Vector3 aPos)
    212.     {
    213.         // find player by aPlayerName, teleport him to aPos.
    214.     }
    215.  
    216.     void WriteLine(string aLine)
    217.     {
    218.         Debug.Log(aLine);
    219.     }
    220. }
    I registered 3 example commands:
    • get [variable name]
    • set [variable name] [new value]
    • tp [player name] [x] [y] [z]
    For simplicity I simply pass the whole splitted command line to each command method. That avoids the splitting into command and arguments. We simply have to access arguments by index 1 since index 0 indicates the actual command.

    The DynVarContainer is just an example to handle and store custom console variables. When using the "set" command, you can actually create new variables. Those are always string values. Apart from that there are different wrapper types which use delegates / closures to represent an actual physical variable as a IDynVar. Setting or getting the dyn var would actually get / set the physical variable (like the "hp" example).

    Note that the whole parser is not very sophisticated. It's quite limited. However you could register almost any command that way. Though it's of course possible to actually define some sort of "argument class", so you could be more specific what parameters are actually supported.
     
    Last edited: Dec 8, 2022