Fast Commands
From SA-MP Wiki
Contents |
Introduction
By far the most common command processing system in terms of use is the strtok method first used for SA:MP in the original LVDM by jax. This exposure from the very start has firmly established it's use in scripts, however it is very slow in comparison to other methods, one of these methods, dcmd, is explained here along with a method of extracting parameters safely and quickly from the entered command.
dcmd
Comparison example
Using strtok:
public OnPlayerCommandText(playerid, cmdtext[]) { new index, cmd[20]; cmd = strtok(cmdtext, index); if (strcmp(cmd, "/heal", true) == 0) { new tmp[20]; tmp = strtok(cmdtext, index); if(!strlen(tmp)) return SendClientMessage(playerid, 0xFF0000AA, "Usage : /heal <ID>"); if(!IsPlayerConnected(strval(tmp))) return SendClientMessage(playerid, 0xFF0000AA, "Player not found"); SetPlayerHealth(strval(tmp), 100.0); SendClientMessage(strval(tmp), 0x00FF00AA, "You have been healed"); SendClientMessage(playerid, 0x00FF00AA, "Player healed"); return 1; } return 0; }
Using dcmd:
public OnPlayerCommandText(playerid, cmdtext[]) { dcmd(heal, 4, cmdtext); return 0; } dcmd_heal(playerid, params[]) { new id; if (!strlen(params)) return SendClientMessage(playerid, 0xFF0000AA, "Usage: \"/heal <playerid>\""); id = strval(params); if (!IsPlayerConnected(id)) return SendClientMessage(playerid, 0xFF0000AA, "Player not found"); SetPlayerHealth(id, 100.0); SendClientMessage(id, 0x00FF00AA, "You have been healed"); SendClientMessage(playerid, 0x00FF00AA, "Player healed"); return 1; }
There is already less code and we've barely done anything, converting the first example into the second took less than 30 seconds and will save a lot of time in processing. Plus the command being in a separate function means you can put it in a separate file if you want and just have a list of commands in OnPlayerCommandText, not a huge load of code.
Explanation
dcmd(heal, 4, cmdtext);
That line basically declares the command, it checks the variable cmdtext to see if you typed /heal, the number 4 is the length of the word "heal", the word "heal" (without double quotes) is both the command to type and part of the function to call.
The function dcmd_heal is the command name you entered in the dcmd line with dcmd_ put in front. It has two parameters, the first, playerid, is the player who typed the command, just like OnPlayerCommandText. The second, params[], is the string entered after the command, this is very similar to cmdtext[] but ONLY contains command parameters (or nothing if no parameters were entered, as shown in the example above).
This is all you need to know to use dcmd, most people are put off by the last remaining part, however this was purposefully left to last as you do not need to understand it at all to use it.
Define line
This is the dcmd define line, just copy the following line to the top of your script, near the includes, and forget about it (Note: may scroll off to the right):
#define dcmd(%1,%2,%3) if (!strcmp((%3)[1], #%1, true, (%2)) && ((((%3)[(%2) + 1] == '\0') && (dcmd_%1(playerid, ""))) || (((%3)[(%2) + 1] == ' ') && (dcmd_%1(playerid, (%3)[(%2) + 2]))))) return 1
zcmd
Information
After using strcmp and dcmd, there is another way of processing commands very fast. We're talking about zcmd. zcmd is the fastest way of processing commands which is available at the moment. You can download the include here: http://forum.sa-mp.com/showthread.php?t=91354 For the speed results see this post: http://forum.sa-mp.com/showpost.php?p=815452&postcount=182
Differences from dcmd
We're already using sscanf here, for the fastest way to process commands.
dcmd_heal(playerid, params[]) { new id; if (sscanf(params, "u", id)) SendClientMessage(playerid, 0xFF0000AA, "Usage: \"/heal <playerid>\""); else if (id == INVALID_PLAYER_ID) SendClientMessage(playerid, 0xFF0000AA, "Player not found"); else { SetPlayerHealth(id, 100.0); SendClientMessage(id, 0x00FF00AA, "You have been healed"); SendClientMessage(playerid, 0x00FF00AA, "Player healed"); } return 1; } CMD:heal(playerid, params[]) { new id; if (sscanf(params, "u", id)) SendClientMessage(playerid, 0xFF0000AA, "Usage: \"/heal <playerid>\""); else if (id == INVALID_PLAYER_ID) SendClientMessage(playerid, 0xFF0000AA, "Player not found"); else { SetPlayerHealth(id, 100.0); SendClientMessage(id, 0x00FF00AA, "You have been healed"); SendClientMessage(playerid, 0x00FF00AA, "Player healed"); } return 1; }
All you actually have to do is CMD:heal(playerid, params[]), you can also do COMMAND:heal(playerid, params[]). The third method is command(heal, playerid, params[]) As you can see it's almost the same, but the code that's behind it is much faster.
sscanf
example revisited
The above example can be simplified even further by the use of sscanf:
public OnPlayerCommandText(playerid, cmdtext[]) { dcmd(heal, 4, cmdtext); return 0; } dcmd_heal(playerid, params[]) { new id; if (sscanf(params, "u", id)) SendClientMessage(playerid, 0xFF0000AA, "Usage: \"/heal <playerid>\""); else if (id == INVALID_PLAYER_ID) SendClientMessage(playerid, 0xFF0000AA, "Player not found"); else { SetPlayerHealth(id, 100.0); SendClientMessage(id, 0x00FF00AA, "You have been healed"); SendClientMessage(playerid, 0x00FF00AA, "Player healed"); } return 1; }
Explanation
sscanf takes two main parameters, the string you want to get information from (in this case params) and the type of information you want to get out (see below). It then takes additional parameters, equal in number to the amount of data you want, in this example we only want one piece of data (the id of the player to heal), players are special so we can use 'u' so people can enter their name OR ID.
If sscanf gets all the data properly it returns 0, so we can check it's not 0 and if it is then it failed and the player typing the command didn't enter the data correctly.
Data types
The second parameter is a list of the types of data required. sscanf can get strings, numbers, characters, floats and hex numbers from a command (or any other string). For example:
/givecash <playerid/partname> <amount>
That is a player and a number, so the list type would be "player number"; numbers are represented by either "i" or "d" (as in format and printf) so the format parameter for that command could be either of:
"ui" "ud"
Another common example is:
/me <action>
The action is just a string so the format parameter is simply "s", "z" is also string, but that's an optional string, an example of which could be:
/ban <playerid> [reason]
The parameters for which are "uz".
Format character | Data type | Example |
---|---|---|
d | Integer | Money |
i | ||
c | Character | A single letter |
u | User ID or name | Player |
s | String | Any length of text |
h | Hex number | Colour |
x | ||
f | Float | Co-ordinate |
z (deprecated) | Optional string. Deprecated, use S instead. | A reason e.g. Ban Reason, only optional at the end of a format string |
p<delimiter> | Splitter | The next character is used as a string delimiter, example:
"p|iii" Will split this string into three integers: 1|2|3 Note that on things other than strings space is still used as well. |
'...' | Match string where ... is text to find | Text you want included but don't use |
Users
The latest version of sscanf introduced the "u" parameter, this replaces "d" or "i" when you want that parameter to be used as a playerid rather than just a normal number. This parameter allows you to type either a number for playerid or part of a name, in which case sscanf will find the first matching player and return their id. It also has checks so if the player doesn't exist or isn't connected it returns INVALID_PLAYER_ID.
Strings
Strings are handled slightly cleverly in sscanf. If the "s" is the last format parameter the string will contain everything remaining in the source string, if it isn't it will just get a single word similar to strtok:
sscanf("hello there you", "s", string);
string will be "hello there you"
sscanf("hello there you", "ss", string1, string2);
string1 will be "hello", string2 will be "there you" (neither string will contain the space between "hello" and "there").
sscanf("hello 0", "si", string, num);
string will contain "hello", num will be 0.
sscanf("hello there 0", "si", string, num);
This sscanf will actually fail as string will get "hello", but "num" isn't a number (it finds "there"), which is the required parameter type.
If you want the last parameter to be a string but only want one word you can simply put a space after the "s" in the parameter string, this will trick sscanf into thinking there's more types wanted but it will just skip the space when it gets to it:
sscanf("hello there", "s ", string);
string will now only contain "hello".
Optional strings
The "z" format parameter when used at the end of a string will create an optional string parameter. It behaves exactly the same as the "s" parameter, especially when used in parts of the format string other than the end, but if the text passed is not long enough the sscanf function still returns 0 for true:
dcmd_ban(playerid, params[]) { new id, reason[64]; if (sscanf(params, "uz", id, reason)) SendClientMessage(playerid, 0xFF0000AA, "Usage: \"/ban <playerid/partname> <reason (optional)>\""); else if (id == INVALID_PLAYER_ID) SendClientMessage(playerid, 0xFF0000AA, "Player not found"); else { BanEx(id, reason); format(reason, sizeof (reason), "You have been banned%s%s.", reason[0] ? (" for: ") : (""), reason); SendClientMessage(id, 0xFF0000AA, reason); SendClientMessage(playerid, 0x00FF00AA, "Player banned"); } return 1; }
Other notes
- sscanf has inbuilt protection against the long number crash (where very long numbers can crash strval).
- IsNumeric is included, sscanf won't try to evaluate strings as numbers (which will just return 0) if they typed the command wrong.
- Simple to add more parameters to a command without major code restructuring.
Code
To use sscanf simply copy the code here somewhere in your mode or download the plugin which is faster.
givecash
The infamous givecash command from LVDM using dcmd and sscanf, this is obviously much shorter than normal implementations. This is a slightly unfair comparison as the sscanf versions actually does MORE than the original in that it can take a player name or a playerid.
public OnPlayerCommandText(playerid, cmdtext[]) { dcmd(givecash, 8, cmdtext); return 0; } dcmd_givecash(playerid, params[]) { new giveplayerid, amount; if (sscanf(params, "ud", giveplayerid, amount)) SendClientMessage(playerid, 0xFF0000AA, "Usage: /givecash [playerid/partname] [amount]"); else if (giveplayerid == INVALID_PLAYER_ID) SendClientMessage(playerid, 0xFF0000AA, "Player not found"); else if (amount > GetPlayerMoney(playerid)) SendClientMessage(playerid, 0xFF0000AA, "Insufficient Funds"); else { GivePlayerMoney(giveplayerid, amount); GivePlayerMoney(playerid, 0 - amount); SendClientMessage(playerid, 0x00FF00AA, "Money sent"); SendClientMessage(giveplayerid, 0x00FF00AA, "Money received"); } return 1; }
Other commands
public OnPlayerCommandText(playerid, cmdtext[]) { dcmd(sethealth, 9, cmdtext); return 0; } dcmd_sethealth(playerid, params[]) { if(IsPlayerAdmin(playerid)) // IsPlayerAdmin check (we want only RCON admins to use this command (error message at bottom if the player isn't logged into RCON) { new id, Float:amount, string[70], pName[MAX_PLAYER_NAME]; // Create the variables needed for this command. id = the id we want to set the health of, amount = the amount we're gonna set id's health to and pName is just where we store id's name. if(sscanf(params, "uf", id, amount)) return SendClientMessage(playerid, 0xFF0000AA, "Usage: /sethealth (id) (amount)"); // Here sscanf checks if the command was typed with the correct usage, it were not, so lets send an error message. // the "ud" part in the code means: u = playerid or part of player's name, f = the float amount we will set ids health to (So you can also type 100.0 instead of just 100) if(!IsPlayerConnected(id)) return SendClientMessage(playerid, 0xFF0000AA, "This player is not connected."); // ID is not connected, send an error message else { GetPlayerName(id, pName, MAX_PLAYER_NAME); // Getting id's name and storing the name in the variable pName SetPlayerHealth(id, amount); // Setting id's health to what the user typed as the amount format(string, sizeof(string), "You've Set %s's (%d) Health To %d.", pName, id, amount); // Formating the string wich will say how much we set id's health to. SendClientMessage(playerid, 0x00FF00AA, string); // Sending the message to the player who typed the command return 1; // The command was successfully processed } } else return SendClientMessage(playerid, 0xFF0000AA, "Looks like you forgot the commands! Use /commands for a list of commands."); // Sending the error message to the player who typed the command because he's not logged into RCON }