[Tutorial] Converting strcmp+strtok commands to zcmd+sscanf
#1

Hey.

The reason I wrote this tutorial is that I had seen a few others on the same subject, yet none of them provided enough information of what's going on (and this is just my opinion). The introduction has been revised in 2013 as I, too, have learned a lot in the past few years, but the main part of this tutorial remains pretty much the way it was in 2011.

I am going to provide some insight on the operations of Zeex's command processor include zcmd and ******' sscanf.

A little history
What used to be the most used method in handling commands until about 2008 was the simple string comparison approach, which means that in OnPlayerCommandText, the input is compared to a bunch of strings and when two happen to be the same, a command is executed.
Later on, DracoBlue introduced dcmd to the community. This method also uses string comparison in the front end and I am not exactly sure about the righteousness of the speed tests in that topic (basically the first method above could be made "as fast" if we were to strip out the string function usage to parse out the parameters).

Introduction of sscanf
sscanf is a string parsing utility function by Alex (******) released in the beginning of 2010. It works somewhat similar to the sscanf of C, but still different, so it has been also called unformat(). Its goal is to parse information out from string input.

Introduction of zcmd and how does it work?
zcmd is a command parser by Zeex released in 2009. Above the surface, what zcmd does is call CallLocalFunction (a SA-MP server native) with the function name being in the format "cmd_x" where x is the command entered by the player transformed into lowercase. Whether the command exists or not (also, whether it was fired or not) is reflected by the return value of CallLocalFunction.
But what does CallLocalFunction do internally? You guessed it, string comparisons it is, but in a bit smarter fashion. The AMX public functions table is ordered in a way it can be searched efficiently using binary search. This means that Zeex's command parser has a search complexity of O(log N) while all old methods have O(N) complexity. This is where the speed difference happens.

Simple command
First, lets convert a command which doesn't actually require sscanf parsing but does have a parameter.
We have a command from Fort Carson Roleplay script - /i (MESSAGE) - which sends a message to IRC.
pawn Код:
if(strcmp(cmd,"/i",true)==0)
{
        if(IsPlayerConnected(playerid))
        {
                if(PlayerInfo[playerid][pPlayersChannel] == 999)
                {
                        SendClientMessage(playerid, COLOR_GREY, "   You are not in an IRC Channel !");
                        return 1;
                }
                if(PlayerInfo2[Mute][playerid] == 1)
                {
                        SendClientMessage(playerid, TEAM_CYAN_COLOR, "You cannot speak, you have been silenced");
                        return 1;
                }
                GetPlayerName(playerid, sendername, sizeof(sendername));
                new length = strlen(cmdtext);
                while ((idx < length) && (cmdtext[idx] <= ' '))
                {
                        idx++;
                }
                new offset = idx;
                new result[128];
                while ((idx < length) && ((idx - offset) < (sizeof(result) - 1)))
                {
                        result[idx - offset] = cmdtext[idx];
                        idx++;
                }
                result[idx - offset] = EOS;
                if(!strlen(result))
                {
                        SendClientMessage(playerid, COLOR_GRAD2, "USAGE: /i [irc chat]");
                        return 1;
                }
                format(string, sizeof(string), "** IRC %s: %s. **", sendername, result);
                SendIRCMessage(PlayerInfo[playerid][pPlayersChannel], COLOR_YELLOW2, string);
        }
        return 1;
}
A major problem needs to be addressed at first: using IsPlayerConnected to see if the command user is online is not necessary. PAWN is single-threaded and how do you think an unconnected player could send a command? So not using zcmd is not the only problem with this code.

The first thing we'll do is move the command out of the OnPlayerCommandText callback scope, and make it a function. While structural well-being is achievable with other methods of command handling, zcmd makes it easy for arranged coders to put the right commands into the right source files.
pawn Код:
if(strcmp(cmd,"/i",true)==0)
changes into
pawn Код:
CMD:i(playerid, params[])
And we continue... The code you see here:
pawn Код:
new length = strlen(cmdtext);
while ((idx < length) && (cmdtext[idx] <= ' '))
{
        idx++;
}
new offset = idx;
new result[128];
while ((idx < length) && ((idx - offset) < (sizeof(result) - 1)))
{
        result[idx - offset] = cmdtext[idx];
        idx++;
}
result[idx - offset] = EOS;
if(!strlen(result))
{
        SendClientMessage(playerid, COLOR_GRAD2, "USAGE: /i [irc chat]");
        return 1;
}
... can all be replaced with a simple isnull check.
pawn Код:
if(isnull(params))
        return SendClientMessage(playerid, COLOR_GRAD2, "USAGE: /i [irc chat]"), true;
Note that strlen(params) may not return 0 in case of no parameters being entered. This is because empty arrays cause CallLocalFunction to misbehave, so "\1\0" is used instead of "\0" in case of no parameters. isnull is defined in the zcmd include and it checks for both cases, \1\0 and \0.

Congratulations! You have converted your first command to use zcmd! With the IsPlayerConnected check removed and some small style changes, here's the outcome properly in one pastebin file.

Command with sscanf usage
Lets say we have a small command with this syntax mandatory: /vehicle destroy/respawn ID. I figured something like this will show you how to parse a word and an ID at the same time, so it should be good.
I'm not even going to figure out what the parsing may look like with strtok - long and messy and would take even me time to code. So I'll move on directly how to write the parameter parsing. You are already familiar with how a zcmd command is defined:
pawn Код:
CMD:vehicle(playerid, params[])
Next, we're going to need some variables and we'll move to creating the sscanf line right away.
pawn Код:
CMD:vehicle(playerid, params[])
{
    new option[8], vID;
    if(sscanf(params, "s[128]d", option, vID))
        return SendClientMessage(playerid, COLOR_YELLOW, "USAGE: /vehicle <destroy/respawn> <ID>"), true;
    // ...
}
What is s[128]d? The sscanf topic has more information about specifiers, but this one is easy:
s[128] - string (our option, whether destory or respawn. Has to be 128 to avoid buffer overflow)
d - decimal (our vehicle ID)
Next on we would need to see if we are being given the proper option parameter:
pawn Код:
if(!strcmp(option, "respawn"))
{

}
else if(!strcmp(option, "destroy"))
{

}
else
    return SendClientMessage(playerid, COLOR_YELLOW, "USAGE: /vehicle <destroy/respawn> <ID>"), true;
The rest is simple: make the parts do the right thing:
pawn Код:
if(!strcmp(option, "respawn"))
{
    SetVehicleToRespawn(vID);
}
else if(!strcmp(option, "destroy"))
{
    DestroyVehicle(vID);
}
Finally, add a
pawn Код:
return true;
in the end and your command is done!
The full version of this command can be found here.

Invalid command entered?
As you know, it is possible to change the unexisting command notice. This can be done in zcmd through the OnPlayerCommandPerformed callback. See an example of how I do it here:
pawn Код:
public OnPlayerCommandPerformed(playerid, cmdtext[], success)
{
    if(!success)
    {
        new tmp[64], str[200]; // I admit, these are global for me!
        sscanf(cmdtext, "s[64] ", tmp);
        str = "ERROR: {FFFFFF}The command {E1DE1C}";
        strcat(str, tmp);
        strcat(str, " {FFFFFF}couldn't be found!");
        SendClientMessage(playerid, 0xFF0000FF, str);
    }
    return true;
}
It is simple - when the command wasn't ran successfully, the success parameter will be 0 (false). To get the starting part of the command without parameters, do sscanf(cmdtext, "s[64] ", ...) with a space after the ].

This is my first tutorial, if something is out of order or you think could be improved, please let me know and I'll be happy to do so. If you're having trouble converting your commands or wish for some tips, feel free to post as well. I'll be trying to help.

Thanks!
Reply
#2

Well done, Andre!
- Dixious
Reply
#3

Great great keep it up!
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)