CounterStrikeSharp icon indicating copy to clipboard operation
CounterStrikeSharp copied to clipboard

Added GetArgs

Open schwarper opened this issue 1 year ago • 11 comments

[ConsoleCommand("css_testx")]
public void OnTest(CCSPlayerController player, CommandInfo info)
{
    var args = info.GetArgs();

    Console.WriteLine($"TESTX args count => {args.Count}");

    for (int i = 0; i < args.Count; i++)
    {
        Console.WriteLine($"{i}. {args[i]}");
    }
}

[ConsoleCommand("css_testy")]
public void OnTest2(CCSPlayerController player, CommandInfo info)
{
    var args = info.GetArgs(0, 3);

    Console.WriteLine($"TESTX args count => {args.Count}");

    for (int i = 0; i < args.Count; i++)
    {
        Console.WriteLine($"{i}. {args[i]}");
    }
}

image

schwarper avatar Nov 07 '24 18:11 schwarper

https://github.com/roflmuffin/CounterStrikeSharp/pull/504

I improved this a bit.

schwarper avatar Nov 07 '24 19:11 schwarper

I'd also like the enumerator version maybe? from #504

KillStr3aK avatar Nov 09 '24 13:11 KillStr3aK

I'd also like the enumerator version maybe? from #504

I thought that getting arg once and split them is better than using GetArg multiple times. However, it might be better.

schwarper avatar Nov 09 '24 23:11 schwarper

Perhaps a solution like this would be the way forward, as I can see calling getarg multiple times might not be the best way forward (my solution), but nor do I think creating a list and a regex expression is (your solution). I made a minor test project to showcase what could become the end result. It kinda combines both yours and mine. The if statement or rather ternary don't need to be implemented, I was just too lazy to copy the original.

   // Emulation of argstring
   private static string ArgString = "my test string";
   // Emulation of ArgCount
   private static int ArgCount = ArgString.Split().Length;
   static void Main(string[] args)
   {
       Console.WriteLine(string.Join(" ", TestMethods(2))); // Output: string
       Console.WriteLine(string.Join(" ", TestMethods(1))); // output: test string
       Console.WriteLine(string.Join(" ", TestMethods(1, 2))); // Output: test
       Console.WriteLine(string.Join(" ", TestMethods(0, 1))); // Output: my
       Console.WriteLine(string.Join(" ", TestMethods())); // Output: test string
   }

   private static IEnumerable<string> TestMethods(int startIndex = 1, int endIndex = -1)
   {

       endIndex = (endIndex > ArgCount || endIndex < 0) ? ArgCount : endIndex;
       startIndex = (startIndex > ArgCount ||  startIndex < 0 || startIndex > endIndex) ? ArgCount : startIndex;
       Console.WriteLine($"Start: {startIndex} | end: {endIndex}");
       
       return ArgString.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).AsEnumerable().Skip(startIndex).Take(endIndex - startIndex);
   }

Splitting with (char[]?)null is reffering to any whitespace as I understand.
The reason for starting startindex at one is, iirc, argstring comes with the command input as first argument, in case of not, my bad. Not sure that "all" of these method calls on enumerable is that performance heavy, but we're avoiding object initialization by using As, which is a thumbs up. Either Take or Skip could be removed by using "TakeWhile" and checking index also.

Glad to see that a PR I made back in June and forgot all about is something that other people (you at least) would have the benefit of 😅

WidovV avatar Nov 13 '24 13:11 WidovV

I think perhaps we are better off just having an IEnumerable<string> Args => that yields in a for loop without any index access, and just pay the performance cost of multiple calls to GetArg(i), that way the user can use regular methods to skip/take or do array accesses from the enumerable.

I'm open to also starting this index at 1 so that we skip the "command" arg.

i.e.

 public static IEnumerable<string> GetValue {
        get
        {
            for (int i = 0; i < ArgCount; i++)
            {
                yield return GetArg(i);
            }
        }
    }

roflmuffin avatar Mar 25 '25 01:03 roflmuffin

I think perhaps we are better off just having an IEnumerable<string> Args => that yields in a for loop without any index access, and just pay the performance cost of multiple calls to GetArg(i), that way the user can use regular methods to skip/take or do array accesses from the enumerable.

I'm open to also starting this index at 1 so that we skip the "command" arg.

i.e.

 public static IEnumerable<string> GetValue {
        get
        {
            for (int i = 0; i < ArgCount; i++)
            {
                yield return GetArg(i);
            }
        }
    }

I don't think it's a good idea to use GetArg in loop. However, I'm (unfortunately) okay with both solutions

schwarper avatar Mar 25 '25 09:03 schwarper

GetValue doesn't quite follow the naming convention since GetArg is a thing, hence GetArgs were the name for the method. I do agree that calling GetArg multiple times instead of splitting ArgString seems like unnecessary performance waste, however I do not think that the first argument should be skipped. An example of usage could be a command listener for every chat message and only wanted to log messages starting with ! or /

WidovV avatar Mar 25 '25 10:03 WidovV

Have we actually determined that the performance of get arg would actually be worse than the string split? The con command on the native side would already know each arg so it's fairly cheap for it to make a copy of the arg and return it back to c#. I'd be interested in a benchmark comparing the two.

roflmuffin avatar Mar 25 '25 21:03 roflmuffin

I've contacted you over dm's on discord regarding how to setup a benchmark to test it 😄

WidovV avatar Mar 26 '25 10:03 WidovV

public static IEnumerable<string> GetArgs(CommandInfo info, int startIndex = 0, int endIndex = -1)
{
    if (string.IsNullOrEmpty(info.ArgString))
        return [];

    string[] args = info.ArgString.Split(' ', StringSplitOptions.RemoveEmptyEntries);
    int lastIndex = args.Length - 1;

    startIndex = Math.Clamp(startIndex, 0, lastIndex);
    endIndex = Math.Clamp(endIndex < 0 ? lastIndex : endIndex, startIndex, lastIndex);

    string[] selectedArgs = startIndex == endIndex
        ? [args[startIndex]]
        : args[startIndex..(endIndex + 1)];

    return selectedArgs.Select(arg =>
        arg.Length >= 2 && arg[0] == '"' && arg[^1] == '"'
            ? arg[1..^1]
            : arg
    );
}

public static IEnumerable<string> GetArgsYield(CommandInfo info)
{
    if (info.ArgCount == 1)
        yield break;

    for (int i = 1; i < info.ArgCount; i++)
        yield return info.GetArg(i);
}

test1 test2

schwarper avatar Apr 01 '25 21:04 schwarper

image

schwarper avatar Apr 01 '25 21:04 schwarper