[Include] Smart Command Processor (Scripter-friendly, feature rich and fast) (aka iZCMD+)
#1

Smart Command Processor BETA
Latest Version: 0.3.2 beta (25th December 2017)

Important Issues:
  1. YSI related includes must be included before SmartCMD for it to work.
What is SmartCMD?
SmartCMD is a fast feature rich command processor which simplifies the creation and handling of commands. SmartCMD (known as iZCMD+ in its initial days), is iZCMD loaded with new features but ironically, it is faster than iZCMD. SmartCMD works on the same principle which iZCMD/ZCMD works on, i.e: create commands as public functions and call the public function when the command is used. However, there are few subtle changes in the core algorithm to make allowances for the new features.

What's new?
  • Command IDs
    SmartCMD assigns a unique ID number to each command to enhance the overall performance. By using command ids instead of command names, costly string comparisons can be avoided.

  • Delete/Undelete commands
    Delete and un-delete commands at runtime.

  • Command Flags
    A command flag is just a variable associated with each command. You can store crucial information about each command in its flag. Flags are optional. Unlike most command processors, command flags can be assigned right next to the command declaration during compile time.

  • Alternate Command Names
    Unlike iZCMD/ZCMD, creating alternate command names does not take any extra CPU. Using the alternate command is as fast as using the parent command. The include provides a simple one liner method to set alternate commands. It also allows you to customize these commands like normal commands.

  • Command Modes/States
    You can have more than one command function associated with each command.

  • Iterating through commands
    Given that the command ids are continuous and the include stockpiles information about every command, it allows iterating through all commands with specific properties. If you'd ever need to implement per-player command permissions or need to store special information specific to each command, it'll take you less than 5 minutes to set things up using this include.

  • Reassign Command Functions
    You can modify command behavior at runtime! The include provides facilities to change a command's function at runtime.

  • Super-friendly include
    SmartCMD allows you to set command properties such as flags, alternate names during compile time while declaring/defining the command itself so that you don't have to fill your OnGameModeInit/OnFilterScriptInit with trash. You can also access command ids and command flags by a simple variable fetch; no need to bloat your code with GetCommandID or Get/SetCommandFlags function calls.

  • Extensible
    The include provides several low level functions which allows anyone to customize the include without directly editing the include. This makes updating easier as your customized functionality is not a part of the include: you won't have to tweak your custom feature code when you update SmartCMD and no code copy-pasting is required.

  • Documentation
    Detailed documentation (with example cod) for every function is present in the include itself. Other aspects of the include have been covered in detail in this topic with several examples.

How to install?
SmartCMD is available as a PAWN include. Download the include from one of the download sources mentioned at the 'download' section of this topic and place it in your "pawno/include" folder.

To add SmartCMD to your script, include 'smartcmd' anywhere in your script (as long as it is included before it's use) using the following code:
Code:
#include <smartcmd>
If you are using a filterscript, ensure that you have FILTERSCRIPT defined before including SmartCMD.

Code:
#define FILTERSCRIPT
How to upgrade to SmartCMD from iZCMD/ZCMD?
Changing commands:
  1. Open Find & Replace Dialog (Ctrl + H)
  2. Provide search term as "(playerid, params[])"
  3. Set the replacement text to "(cmdid, playerid, params[])"
  4. Click on Replace All
Changing callbacks:
  1. Go to your OnPlayerCommandReceived declaration if it exists
  2. Change the declaration to "OnPlayerCommandReceived(cmdid, playerid, cmdtext[])"
  3. Go to your OnPlayerCommandPerformed declaration if it exists
  4. Change the declaration to "OnPlayerCommandPerformed(cmdid, playerid, cmdtext[], success)"
Converting iZCMD/ZCMD alternate commands to SmartCMD alternate commands:
  1. Add "ALT:alternate_name = CMD:original_name;" anywhere in your script outside executable blocks (functions).
  2. Remove the old iZCMD/ZCMD type alternate command
Replacing manual command function calls:
  1. Open Find Dialog (Ctrl + F)
  2. Search for "cmd_"
  3. Replace "cmd_urcmd(playerid, params)" with "cmd_urcmd(cid_urcmd, playerid, params)".
You can upgrade your code using SmartCMD features as you like. For example, you can assign admin levels to command using flags and check the sender's admin level in OnPlayerCommandReceived. You can do the same for other class specific commands. You can do a lot with this include. It is up to you to decide how you'll make the best use of this include.

Creating commands
You can create commands in four different ways. All the forms of command declarations are functionally identical.

For every command you create, SmartCMD creates a public function with the command's code (check Technical Details section to know more). This public function is known as the command('s) function. SmartCMD also assigns a unique number to each command which is known as command id. An invalid command id is identified with INVALID_COMMAND_ID.

Code:
COMMAND:yourcommand(cmdid, playerid, params[])
{
      return CMD_SUCCESS;
}
CMD:yourcommand(cmdid, playerid, params[])
{
      return CMD_SUCCESS;
}
command(yourcommand, cmdid, playerid, params[])
{
      return CMD_SUCCESS;
}
cmd(yourcommand, cmdid, playerid, params[])
{
      return CMD_SUCCESS;
}
When a player types "/yourcommand parameters", the command function will be called. The 'cmdid' parameter gives you the unique command id which you can use to manipulate the command and its properties. The 'playerid' parameter will have the id of the player who used the command. The 'params' array will have the text which the player typed after the command (in the given example, params will have "parameters"). If any leading spaces were present in the parameters, they will be absent in 'params' array.

You must return CMD_SUCCESS (or 1) if the command was executed successfully and CMD_FAILURE (or 0) if the command failed to execute properly. If there is no return statement in your command or if the command crashes during execution, CMD_FAILURE will be returned by default.

You can return '1' in place of CMD_SUCCESS and return '0' in place of CMD_FAILURE if you wish to.

Adding default flags
Unlike other command processors, SmartCMD allows you to give default flags for commands while writing the command itself. It is 'optional', you can write a command without providing default flags and it will still compile. The flags of such commands will be initialized with zero (the default value for default flags can be changed by defining CMD_DEFAULT_FLAG).

Code:
enum (*=2)
{
     ADMIN_COMMAND = 1,
     VIP_COMMAND,
     DEBUG_COMMAND
}
COMMAND<ADMIN_COMMAND>:ban(cmdid, playerid, params[])
{
  return CMD_SUCCESS;
}
CMD<ADMIN_COMMAND>:ban(cmdid, playerid, params[])
{
  return CMD_SUCCESS;
}
command<ADMIN_COMMAND>(ban, cmdid, playerid, params[])
{
  return CMD_SUCCESS;
}
cmd<ADMIN_COMMAND>(ban, cmdid, playerid, params[])
{
  return CMD_SUCCESS;
}
That's it! No junk in OnGameModeInit or OnFilterScriptInit or anywhere else.

The flags are stored in a hidden variable which identifies itself with the command name prefixed with 'flg_'. This variable can be accessed and modified by you at runtime.

Code:
CMD:test(cmdid, playerid, params[])
{
     printf("This command's flags variable has the value: %d", flg_test);
     flg_test = ADMIN_COMMAND; //You can even change the flags by directly assigning values to the flag variable
     return CMD_SUCCESS;
}
When you don't know the command's name beforehand or when you are dealing with a general case where the command could be any command, you can obtain/modify the flags using GetCommandFlags(cmdid) and SetCommandFlags(cmdid, flags) functions.

As the name suggests, GetCommandFlags(cmdid) is used to access a command's flags and SetCommandFlags(cmdid, flags) is used to set a command's flags.

In an earlier example, flags was used as a bitmask. However, you have all the freedom to use it in whatever way you want as the flags is essentially a cell/variable associated with each command. It can be used to store an integer, float, etc.

Adding alternate command names:
SmartCMD introduces a new syntax for adding alternate names to commands. Calling the alternate command is as good as calling the actual command itself performance-wise.

Code:
CMD:spectate(cmdid, playerid, params[])
{
    //even when you use spec, sp, the cmdid passed to spectate function will always be that of the spectate command
    return 1;
}
ALT:spec = CMD:spectate;
ALT:sp = CMD:spectate;

ALT:alternate_name = CMD:original_name;
When a player uses an alternate command, SmartCMD directly calls the parent command, unlike iZCMD/ZCMD where the alternate command is first called which then calls the parent command.

The command function of the parent command which the alternate command points to (spectate in the above case), when called via an alternate command (spec, sp, view in the above case), will receive the command id of the parent command. There is no way the command function can find out if the alternate command was used or if the parent command was used. However, you must note that the alternate commands do have their own unique command id. This allows you to modify properties of an alternate command without affecting it's parent command. The alternate command also has it's own flags variable. The flags variable is initialized with the flags of it's parent command just once during initialization. Any change made to the flags of any alternate command won't affect the parent command or vice versa.

You can make two separate distinguishable commands which share the same code using the function reassignment feature. When a reassigned command is called, the parent command will be called with the alternate command's command id (you can't call it an alternate command anymore; it is just a command which has its command function assigned to another function).

In such cases, you can either use GetCommandID or use the scripter-friendly command id variables to check which command was actually used by the player.

Code:
//abc is not an alternate command to xyz but has it's command function reassigned to that of xyz
CMD:xyz(cmdid, playerid, params[])
{
      if(cmdid == cid_xyz) //directly using variables is faster than using GetCommandID
      {
              //player used /xyz
      }
      else if(cmdid == cid_abc) //cid_xyz and cid_abc are defined by SmartCMD
      {
             //player used /abc
      }
      return CMD_SUCCESS;
}
Every command has an id variable which is identified as command name prefixed with "cid_". These are fixed constants which cannot be modified.

Code:
CMD:xyz(cmdid, playerid, params[])
{
      printf("The command id of xyz is %d", cid_xyz);
      cid_xyz = 123; // error - you cannot modify a constant variable
      return CMD_SUCCESS;
}
Adding additional command functions
You can create any number of additional functions for a command. These are also known as command modes or command states.

The syntax for creating an additional command function is as follows:

Code:
CMD:ban[params](cmdid, playerid, params[]) //Flags for additional command functions are redundant.
        return SendClientMessage(playerid, -1, "/ban [playerid/name] [reason]");
CMD<ACMD>:ban[help](cmdid, playerid, params[]) //You can still provide the flag if you want. It has no meaning however, i.e: it wont be used. 
        return SendClientMessage(playerid, -1, "Bans the player.");
CMD<ACMD>:ban(cmdid, playerid, params[]) //all the additional command function use the flags 
{
     //ban code
     return CMD_SUCCESS;
}
The flags of an additional command function have no meaning, they are redundant. They are there for the sake of providing the choice of readability, it matters for those who would prefer to keep the flags in additional command functions for consistency.

There must always be a command function without any specific mode. This will be the command function executed when the player types the command.

Each additional command mode/state is given a unique name inside the square brackets ([ ]). To execute an additional command function, use have to use ExecuteCommand function.

Code:
ExecuteCommand("ban", state_name, playerid, "params");
Callbacks
When a player sends a command, SmartCMD calls OnPlayerCommandReceived (if you have defined it in your script) before executing the command where you can decide if the command has to be executed or not. If the command function is allowed to execute in OnPlayerCommandReceived, SmartCMD will call the command function. After the command function finishes executing, SmartCMD will call OnPlayerCommandPerformed (if you have defined it in your script) with necessary details including the return value of the command function.

OnPlayerCommandReceived
Arguments:
  • cmdid: Command ID of the command used by the player. If the entered command does not exist, cmdid will be set to INVALID_COMMAND_ID.
  • playerid: ID of the player who used the command
  • cmdtext: The exact text which the player had typed in the message bar (for example, "/ban 0 test")
Return Values:
1 - Will allow the execution of the command function if it exists. If the command does not exist, OnPlayerCommandPerformed will be called with the "success" parameter as CMD_FAILURE.
0 - Will prevent the execution of the command function.

Code:
public OnPlayerCommandReceived(cmdid, playerid, cmdtext[])
{
        if(cmdid == INVALID_COMMAND_ID)
        {
                SendClientMessage(playerid, RED, "You typed an invalid command.");
                //return 1; //will call OnPlayerCommandPerformed with success value as CMD_FAILURE. 
                return 0; //won't call OnPlayerCommandPerformed
        }      
	return 1;
}
OnPlayerCommandPerformed
Arguments:
  • cmdid: Command ID of the command used by the player. If the entered command does not exist, cmdid will be set to INVALID_COMMAND_ID.
  • playerid: ID of the player who used the command
  • cmdtext: The exact text which the player had inputted into the message bar (for example, "/ban 0 test")
  • success: The value returned by the command function if it was existed. If it did not exist, success will be set to CMD_FAILURE by default.
Return Values:
0 or CMD_FAILURE - passes the command to other scripts for processing. If all the scripts return CMD_FAILURE or 0, the player will see the default error message, i.e "Unknown command".
1 or CMD_SUCCESS - stops the processing (does not pass the command to the next script in line)

Code:
public OnPlayerCommandPerformed(cmdid, playerid, cmdtext[], success)
{
        //a valid command but failed to complete? maybe it crashed?
        if(cmdid != INVALID_COMMAND_ID && !success) return SendClientMessage(playerid, RED," Oops! The command failed to execute properly."), 1; //write to the crash log?
	return 1; //won't send the default error message
}
If you are not using OnPlayerCommandPerformed then what you return in your command function will decide if the error message will be sent or not.

Returning 0 or CMD_FAILURE in the command function means the error message will be sent.
Returning 1 or CMD_SUCCESS in the command function means the error message won't be sent.

Defines & Macros

Defines

The defines marked with an asterisk ( '*' ) can be defined by you before including if you wish to (if not defined, the default value will be assumed). If you are defining any of them yourself, they must be done before including SmartCMD so that SmartCMD is aware of the change.

#definedefaultdescription
1MAX_PUBLIC_FUNCTIONS*1024Maximum number of public functions. If you script contains more public functions than the value set to MAX_PUBLIC_FUNCTIONS, SmartCMD will send an error message in the console in runtime. In that case, increase the value of this define.
2MAX_COMMANDS*500Maximum number of commands. If you script contains more commands than the value set to MAX_COMMANDS, SmartCMD will send error messages in the console. In that case, increase the value of this define.
3MAX_CLIENT_MSG_LENGTH144Maximum length of client message.
4MAX_FUNC_NAME32Maximum length of a PAWN function identifier (including null terminator).
5MAX_COMMAND_NAME28Maximum length of a command name (including null terminator).
6INVALID_COMMAND_ID-1Used to refer to an invalid command.
7IZCMD_ENABLE_CASE_SENSITIVITY*not definedDefining IZCMD_ENABLE_CASE_SENSITIVITY will make commands case-sensitive ("/pm" and "/PM" will be considered as different commands).
8CMD_DEFAULT_FLAG*not definedBy default, the flags will be initialized to zero if they are not specified for the command. This define lets you change the default value of default flags.
9CMD_SUCCESS1
10CMD_FAILURE0
Macros
isnull
Checks if a string is empty/null.

Parameters:
string: The string which has to be checked
Returns:
true - if the string is null ("" or "\1")
false - if the string is not empty

Code:
CMD:pm(cmdid, playerid, params[])
{
      if(isnull(params)) return SendClientMessage(playerid, YELLOW, "/pm [playerid/name]");

      //you can also use the following
      if(params[0] == 0) return SendClientMessage(playerid, YELLOW, "/pm [playerid/name]");

      return CMD_SUCCESS;
}
Functions
Detailed description of each function can be found inside the include.

Code:
	native DoesCommandExist(cmdid)
	native GetCommandID(const cmd[])
	native GetCommandName(cmdid, cmd[], len = sizeof(cmd))
	native GetAlternateCommands(cmdid, cmdidlist[])
	native IsCommandAlternate(cmdid)
	native GetCommandFunctionID(cmdid)
	native GetPointingCommandFunctionID(cmdid)
	native GetPointingCommandID(cmdid)
	native GetCommandFunctionName(dest[], cmdid, len = sizeof(dest))
	native GetEnabledCommandCount()
	native GetDisabledCommandCount()
	native GetTotalCommandCount()
	native EnableCommand(cmdid)
	native DisableCommand(cmdid)
	native IsCommandEnabled(cmdid)
	native SetCommandFlags(cmdid, flags)
	native GetCommandFlags(cmdid)
        native SetPointingCommandIDToSelf(cmdid)
	native ReassignCommandFunction(cmdid, const funcname[], bool:updateCID = false, bool:updatePFT = false)
	native EmulateCommandEx(cmdid, playerid, params[])
	native EmulateCommand(playerid, cmdtext[])
	native ExecuteCommand(const cmd[], command_mode, playerid, &success, params[]="")
Applications of SmartCMD features
  • command which creates a list of commands which updates itself when a command is added or removed
    It is a boon for all of us if we had a /cmds command which updates itself when a new command is added. You can make such smart commands with SmartCMD. The /acmds command is supposed to list all administrative commands available in the server. We mark all administrative commands with the ACMD flag which /acmds uses to prepare a list of all admin commands.

    Code:
    #define ACMD 1
    CMD<ACMD>:ban(cmdid, playerid, params[])
    {
          //Ban Code
          return CMD_SUCCESS;
    }
    CMD<ACMD>:kick(cmdid, playerid, params[])
    {
          //Kick Code
          return CMD_SUCCESS;
    }
    CMD<ACMD>:acmds(cmdid, playerid, params[])
    {
         static str[512], bool:need_frmt = true;
         if(need_frmt)
         {
              new altlist[32]; //A command can have a maxiumum of 32 alternate names? it is your script and you know it!
              for(new i = 0, j = GetTotalCommandCount(); i < j; i++)
              {
                    if(i == cmdid || IsCommandAlternate(i)) continue; //Lets not add /acmds itself to the list. And also let us not add alternate commands as separate commands.
                    if(GetCommandFlags(i) == ACMD)
                    {
                          new cmd[MAX_COMMAND_NAME];
                          GetCommandName(i, cmd);
                          strcat(str, cmd);
                          for(new j = GetAlternateCommands(i, altlist); j; j--)
                          {
                                strcat(str, ",");
                                GetCommandName(altlist[j], cmd);
                                strcat(str, cmd);
                          } 
                          strcat(str, "\n");
                    }  
              }
              need_frmt = false;
         }
         return ShowPlayerDialog(playerid, ACMDS_DIALOG, DIALOG_STYLE_LIST, "Admin Commands", str, "Select", "Cancel");
    }
  • Restricting access to administrative commands
    This is another boon to all of us. You don't have to check the player's admin level in each and every admin command. You need to do just one check in OnPlayerCommandReceived. We mark all admin commands with the ACMD flag. When a player sends a command, OnPlayerCommandReceived is called before the command function is executed. Therefore, we check the flags of the command to see if it is an admin command in OnPlayerCommandReceived. If it is an admin command, we then check if the player is an admin. If the player is an admin, we let the command function to execute. If the player isn't an administrator, we send an error message informing the player that he cannot use the command.

    This is faster as the command function won't even be called if the player is not an administrator unlike other command processors where the admin level checks are done inside the command function. Moreover, it keeps your code neat.

    Code:
    enum (*=2)
    {
          ACMD = 1
    }
    CMD<ACMD>:ban(cmdid, playerid, params[])
    {
          Ban(playerid);
          return CMD_SUCCESS;
    }
    public OnPlayerCommandReceived(cmdid, playerid, cmdtext[])
    {
         if(GetCommandFlags(cmdid) == ACMD)
              if(!IsPlayerAdmin(playerid))
              {
                   SendClientMessage(playerid, RED, "You are not allowed to use this command.");
                   return 0; //Do not execute the admin command
              }
        return 1; //Execute the command
    }
  • Disabling commands in-game (Error message for disabled commands)
    By using the DisableCommand function, you are essentially deleting the command.

    Code:
    CMD<ACMD>:disablecmd(cmdid, playerid, params[])
    {
          switch(DisableCommand(GetCommandID(params)))
          {
                 case -1: return SendClientMessage(playerid, RED, "The entered command does not exist.");
                 case 0: return SendClientMessage(playerid, RED, "The entered command is already disabled.");
                 case 1: return SendClientMessage(playerid, RED, "The entered command has been disabled.");
          }
    }
    CMD<ACMD>:enablecmd(cmdid, playerid, params[])
    {
          switch(EnableCommand(GetCommandID(params)))
          {
                 case -1: return SendClientMessage(playerid, RED, "The entered command does not exist.");
                 case 0: return SendClientMessage(playerid, RED, "The entered command is already enabled.");
                 case 1: return SendClientMessage(playerid, RED, "The entered command has been enabled.");
          }
    }
  • Disabling commands in-game (Disabled message for disabled commands)
    In the previous example, by disabling the command, you were deleting it. When a player uses the command, it will treated as if he typed an invalid command. This example is more user friendly. Instead of deleting the command, we change the command function of the command. We first create a dummy command function which sends a message to the player informing him that the command has been disabled. We then assign the dummy command function to the command that has to be disabled. Hence, whenever a player uses the disabled command, the dummy command function will be called, which sends the disabled message.
    Code:
    forward disabled(cmdid, playerid, params[])
    public disabled(cmdid, playerid, params[])
    {
          return SendClientMessage(playerid, RED, "The command has been disabled.");
    }
    CMD<ACMD>:disablecmd(cmdid, playerid, params[])
    {
          switch(ReassignCommandFunction(GetCommandID(params), "disabled"))
          {
                 case -1: return SendClientMessage(playerid, RED, "The entered command does not exist.");
                 case 0: return SendClientMessage(playerid, RED, "The entered command is already disabled.");
                 case 1: return SendClientMessage(playerid, RED, "The entered command has been disabled.");
          }
          return CMD_SUCCESS;
    }
    CMD<ACMD>:enablecmd(cmdid, playerid, params[])
    {
          new cmd_func[MAX_FUNC_NAME], cid = GetCommandID(params);
          GetCommandFunctionName(cid, cmd_func);
    
          switch(ReassignCommandFunction(cid, cmd_func))
          {
                 case -1: return SendClientMessage(playerid, RED, "The entered command does not exist.");
                 case 0: return SendClientMessage(playerid, RED, "The entered command is already enabled.");
                 case 1: return SendClientMessage(playerid, RED, "The entered command has been enabled.");
          }
          return CMD_SUCCESS;
    }
  • Custom Invalid Command Message
    Since SmartCMD lets you know if a command is invalid in OnPlayerCommandReceived itself, some processing time is saved since attempting to call the command function and then calling OnPlayerCommandPerformed is avoided.

    Code:
    public OnPlayerCommandReceived(cmdid, playerid, cmdtext[])
    {
         if(cmdid == INVALID_COMMAND_ID) 
         {
               SendClientMessage(playerid, RED, "The command you entered does not exist. Use /cmds to obtain a list of commands");
               return 0; //No need to call OnPlayerCommandPerformed.
         }        
         return 1; //Execute the command
    }
  • Logging command crashes & player-friendly crash messages
    You can use SmartCMD to log command function failures, crashes, etc with essential information such as date/time, command name, player name and the exact command text. By saving the player's name, you can talk to the player next time you meet to collect more information.

    Code:
    public OnPlayerCommandPerformed(cmdid, playerid, cmdtext[], success)
    {
         if(cmdid != INVALID_COMMAND_ID && !success) //command exists but it failed
         {
               new str[256], cmd[MAX_COMMAND_NAME], day, month, year, second, minute, hour;
               GetCommandName(cmdid, cmd); 
               getdate(year, month, day);
               gettime(hour, minute, second);
               format(str, sizeof(str), "[%d:%d:%d - :%d:%d:%d][COMMAND CRASH] Command Name:%s Player:%s(%d) Cmdtext:%s", day, month, year, hour, minute, second, cmd, PlayerInfo[playerid][Name], cmdtext);
    
               new File:crashlog = fopen("cmd_crashes.txt", io_append);
               fwrite(crashlog, str);
               fclose(crashlog);
    
               SendClientMessage(playerid, YELLOW, "Oops! Sorry! The requested command failed to execute properly.");
               return 1;
         }        
         return 1; 
    }
  • Per-player command permissions
    Code:
    new PlayerDefaultCommandPermission[MAX_COMMANDS]; //Default permissions - note that even this can be edited in-game
    new PlayerCommandPermission[MAX_PLAYERS][MAX_COMMANDS]; //true = allowed, false = not allowed
    
    pubic OnPlayerConnect(playerid)
    {
         memcpy(PlayerCommandPermission[playerid], PlayerDefaultCommandPermission, MAX_COMMANDS*4); //Set player command permissions to default command permissions
         return 1;
    }
    CMD<ACMD>:disablepcmd(cmdid, playerid, params[])
    {
          new pid, cid, cmd[MAX_COMMAND_NAME];
          if(sscanf(params, "us["#MAX_COMMAND_NAME"]", pid, cmd)) return SendClientMessage(playerid, GREY, "Correct Usage: /disablepcmd [playerid/name] [command name]");
    
          cid = GetCommandID(cmd);
          if(cid == INVALID_COMMAND_ID) return SendClientMessage(playerid, RED, "The command you wished to disable for the player does not exist.");
    
          PlayerCommandPermission[pid][cid] = false;
          return SendClientMessage(playerid, GREEN, "The given command has been disabled for the given player.);
    }
    CMD<ACMD>:enablepcmd(cmdid, playerid, params[])
    {
          new pid, cid, cmd[MAX_COMMAND_NAME];
          if(sscanf(params, "us["#MAX_COMMAND_NAME"]", pid, cmd)) return SendClientMessage(playerid, GREY, "Correct Usage: /enablepcmd [playerid/name] [command name]");
    
          cid = GetCommandID(cmd);
          if(cid == INVALID_COMMAND_ID) return SendClientMessage(playerid, RED, "The command you wished to enable for the player does not exist.");
    
          PlayerCommandPermission[pid][cid] = true;
          return SendClientMessage(playerid, GREEN, "The given command has been enabled for the given player.");
    }
    public OnPlayerCommandReceived(cmdid, playerid, cmdtext[])
    {     
         if(cmdid != INVALID_COMMAND_ID && !PlayerCommandPermission[playerid][cmdid])
         {
               SendClientMessage(playerid, RED, "You are not allowed to use this command."); 
               return 0;
         }
         return 1;
    }
  • Making a /help [cmd] command
    Code:
    CMD:report[help](cmdid, playerid, params[]) return SendClientMessage(playerid, -1, "Reports the player to an administrator.");
    CMD:report(cmdid, playerid, params[]) {...}
    
    CMD:pm[help](cmdid, playerid, params[]) return SendClientMessage(playerid, -1, "Sends a private message to the given player.");
    CMD:pm(cmdid, playerid, params[]) {...}
    
    CMD:help[help](cmdid, playerid, params[]) return SendClientMessage(playerid, -1, "Access server help topics.");
    CMD:help(cmdid, playerid, params[]) 
    {
          new success;
          ExecuteCommand(params, help, playerid, success, "");
          switch(success)
          {
               case INVALID_COMMAND_ID: return SendClientMessage(playerid, RED, "The specified command does not exist.");
               case 0:  return SendClientMessage(playerid, RED, "The specified command does not have a help description.");
               case 2: return SendClientMessage(playerid, RED, "The specified command does not exist."); //command was deleted
    //case 1: handled by the command which was executed, i.e: a description message was sent to the player
          }
          return CMD_SUCCESS;
    }
  • Using flags as bitmask
    As said earlier, the flags are just variables/cells like any other. You can store anything in the flags in any fashion.

    Code:
    enum (*=2)
    {
        CFLAG_ACMD = 1,
        CFLAG_VIP,
        ADMIN_LEVEL_0,
        CALEVEL_1,
        CALEVEL_2,
        CALEVEL_3,
        //store admin level of the admin command in these 3 bits; admin level can start from 0 and go upto 7
        .
        .
        .
        //total number should not exceed 32
    }
    
    //we will use CALEVEL_1, CALEVEL_2, CALEVEL_3 to store the admin level as a number
    //if you understand how to write a decimal number in binary form, the following defines must be self explainatory
    #define ADMIN_LEVEL_1 CALEVEL_1 //1<<3
    #define ADMIN_LEVEL_2 CALEVEL_2 //2<<3
    #define ADMIN_LEVEL_3 CALEVEL_2 | CALEVEL_1 //3<<3
    #define ADMIN_LEVEL_4 CALEVEL_3 //4<<3
    #define ADMIN_LEVEL_5 CALEVEL_3 | CALEVEL_1 //5<<3
    #define ADMIN_LEVEL_6 CALEVEL_3 | CALEVEL_2 //6<<3
    //and so on
    
    stock GetAdminLevel(flags)
    {
           return ((flags & (CALEVEL_1 | CALEVEL_2 | CALEVEL_3))>>>3);
    }
    CMD<ADMIN_LEVEL_2 | CFLAG_ACMD | CFLAG_VIP>:nitros(cmdid, playerid, params[])
    {
          //add nos code
          return CMD_SUCCESS;
    }
    stock IsVIPCommand(cmdid)
    {
          if(GetCommandFlags(cmdid) & CFLAG_VIP) return true;
          return false;
    }
    In this way, you can store more than one information in flags.

  • Restricting access to administrative commands based on the player's admin level
    For those who are completely new to bit manipulation, here is a simple way to use the command id system to associate every administrative command with an administrative level. If you are not using flags for any other purpose, you can use the flags itself as a access level indicator (i.e: use flags of every command to store the admin level as an integer).

    Code:
    flg_kick = ADMIN_LEVEL_2;
    flg_ban = ADMIN_LEVEL_3;
    but using the above method wont allow you to use the flags for any other purpose. Therefore, it is advisable not to use the whole flags to store the admin level as an integer so as to future proof your code.

    So here is another way.

    Code:
    enum (*=2)
    {
        VIP_CMD = 1,
        RCON_CMD,
        ACMD1,
        ACMD2,
        ACMD3,
        ACMD4,
        ACMD5
    }
    
    new AdminLevel[MAX_COMMANDS] = {0,0 ,0, ...};
    
    CMD<ACMD1>:spectate(cmdid, playerid, params[])
    {
          return 1;
    }
    CMD<ACMD2>:kick(cmdid, playerid, params[])
    {
          return 1;
    }
    CMD<ACMD3>:ban(cmdid, playerid, params[])
    {
          return 1;
    }
    CMD<ACMD4>:banip(cmdid, playerid, params[])
    {
          return 1;
    }
    CMD<ACMD5>:setlevel(cmdid, playerid, params[])
    {
          return 1;
    }
    
    public OnGameModeInit()
    {
          for(new i = GetTotalCommandCount()-1; i >= 0; i--)
          {
                 new flags = GetCommandFlags(i);
                 if(flags & ACMD1) AdminLevel[i] = 1;
                 else if(flags & ACMD2) AdminLevel[i] = 2;
                 else if(flags & ACMD3) AdminLevel[i] = 3;
                 else if(flags & ACMD4) AdminLevel[i] = 4;
                 else if(flags & ACMD5) AdminLevel[i] = 5;
          }
    }
    
    public OnPlayerCommandReceived(cmdid, playerid, cmdtext[])
    {
            if(cmdid == INVALID_COMMAND_ID) return SendClientMessage(playerid, RED, "You typed an invalid command. Use /cmds to obtain a list of commands.");
    
            if(PlayerAdminLevel[playerid] < AdminLevel[cmdid]) return SendClientMessage(playerid, RED, "You are not allowed to use this command."), 0;
    }
    Here is another way to do the same. (the best method in my opinion)
    Code:
    enum (*=2)
    {
        VIP_CMD = 1,
        RCON_CMD,
        PLAYER_CMD.
        ACMD1,
        ACMD2,
        ACMD3,
        ACMD4,
        ACMD5
    }
    
    #define 
    
    CMD<ACMD1>:spectate(cmdid, playerid, params[])
    {
          return 1;
    }
    CMD<ACMD2>:kick(cmdid, playerid, params[])
    {
          return 1;
    }
    CMD<ACMD3>:ban(cmdid, playerid, params[])
    {
          return 1;
    }
    CMD<ACMD4>:banip(cmdid, playerid, params[])
    {
          return 1;
    }
    
    CMD<ACMD5>:setlevel(cmdid, playerid, params[])
    {
          return 1;
    }
    
    public OnPlayerCommandReceived(cmdid, playerid, cmdtext[])
    {
             static AdminLevelFlag[6] = {	PLAYER_COMMAND, //normal player command
    	 								PLAYER_COMMAND | ACMD1,
    									PLAYER_COMMAND | ACMD1 | ACMD2, //allow level 2 admins to use level 1 commands
    									PLAYER_COMMAND | ACMD1 | ACMD2 | ACMD3, //allow level 3 admins to use level 1 and 2 commands as well
    									PLAYER_COMMAND | ACMD1 | ACMD2 | ACMD3 | ACMD4,
    									PLAYER_COMMAND | ACMD1 | ACMD2 | ACMD3 | ACMD4 | ACMD5};
    
            if(cmdid == INVALID_COMMAND_ID) return SendClientMessage(playerid, RED, "You typed an invalid command. Use /cmds to obtain a list of commands.");
    
    //Check if this particular admin level has access to the command
            if(!(GetCommandFlags(cmdid) & AdminLevelFlag[PlayerAdminLevel[playerid]])) return SendClientMessage(playerid, RED, "You are not allowed to use this command.") ,0
    
           return 1;
    }
Technical Details (Optional)
Anything which can be typed and prefixed with a '/' is known as a command. The commands which can be processed are known as valid commands and the ones which don't exist are known as invalid commands. By this definition, alternate names for commands are separate commands, i.e: if you had a /spectate command which has an alternate name as /spec then /spec would be a different command but it works as an alternate for the /spectate command.

Commands & Command ID:
Every command including alternate commands is assigned a unique number known as command id. If you had a spectate command which has alternate names such as spec, spectate and spec will have different command id. This allows you to modify properties (disable, reassign, etc) of alternate commands without affecting its parent command.

The command ids start from 0 and can go upto a maximum of MAX_COMMANDS. You can get the highest command id by subtracting one from GetTotalCommandCount().

Code:
for(new i = 0, j = GetTotalCommandCount(); i < j; i++)
{
     //Loop through all the commands
}
Every command has a public function associated with it known as command function. SmartCMD converts your code into proper PAWN code for compiling.

Code:
CMD:pm(cmdid, playerid, params[])
{
       return CMD_SUCCESS;
}
CMD<ACMD>:kick(cmdid, playerid, params[])
{
       return CMD_FAILURE;
}
ALT:kik = CMD:kick;
The above code translates to

Code:
forward _CCCM:tag@pm();
public flg_pm = CMD_DEFAULT_FLAG;
stock flg@pm=flg_pm; //To avoid unused symbol warnings
public stock const cid_pm = -1; //Yes, it is -1. SmartCMD will later initilize this variable with the correct command id.
forward cmd_pm(cmdid, playerid, params[]);
public cmd_pm(cmdid, playerid, params[]) <> return izcmd_cmd_handled:0;
public cmd_pm(cmdid, playerid, params[]) <cmdMode:normal>
{
     return CMD_SUCCESS;
}

forward _CCCM:tag@kick();
public flg_kick = (ACMD);
stock flg@kick=flg_kick;
public stock const cid_kick = -1;
forward cmd_kick(cmdid, playerid, params[]);
public cmd_kick(cmdid, playerid, params[]) <> return izcmd_cmd_handled:0;
public cmd_kick(cmdid, playerid, params[]) <cmdMode:normal>
{
     return CMD_SUCCESS;
}

public flg_kik = (CMD_DEFAULT_FLAG); //SmartCMD will later initilize this variable with the flags of its parent command, in this case, flags of kick.
stock flg@kik=flg_kik;
public stock const cid_kik = -1;
public alt_kik = -1;
stock alt@kik = alt_kik;
forward cmd_kik(cmdid, playerid, params[]);
public cmd_kik(cmdid, playerid, params[]) <> return izcmd_cmd_handled:0;
public cmd_kik(cmdid, playerid, params[]) <cmdMode:normal> //will be executed automatically during initilization
         return _izcmdp_register_alias(GetCommandID("kick"), cmdid);
If case-sensitivity is not enabled, all command names must be written in small caps.

There are two ways in which a command can call the command function of some other command.

Alternate Commands: These commands pretend to be the other command. Using /spec is identical to using /spectate. When a player uses the /spec command, the /spectate command will be called with command id that of /spectate. There is no way you can know if the player used /spectate or /spec.

Reassigned Commands: These commands call the command function of the other command but do not pretend to be that command. Suppose, /spec instead of being an alternate command to /spectate had itself reassigned to /spectate. When /spec is used by a player, the /spectate command function is called BUT "the command id passed to the /spectate command will be that of /spec".
  • Pointing command id: The id of the command to which the command points to. For example, the alternate command /spec points to /spectate. Note that reassignment of command function does not make it point to another command by default rather it just changes the command's command function.
  • Actual/original command id: This is the command id of the command itself and is no way related to what it points. For example, /spec has its own command id and this is called the actual/original command id but it points to the /spectate command.
The command id can be accessed directly by referencing the id variable. The id variable's identifier is the command name itself prefixed with "cid_", for example, "cid_spectate".

In cases where you do not know the command name beforehand, you can use GetCommandID function to obtain the command id from its name.

Function Index:
Every command has its own command function including alternate commands (command functions of alternate commands are defunct - they exist for initialization purposes only and cannot be used unless they are reassigned).

The command function identifies itself with the command name prefixed with "cmd_". You can call the command function directly as given below

Code:
cmd_test(cid_test, playerid, params[]); //calls test command
Using the above method will not trigger the OnPlayerCommandReceived or OnPlayerCommandPerformed callback. Use EmulateCommand(Ex) if you want the callbacks to be executed.
  • Pointing function index: The index of the function the command points to. Both alternate commands and reassigned commands point to the function which it executes when used.
  • Actual/original function index: This value is a number which gives the position of the command in a sorted list of commands. This function index is usually the index of the command function at startup. This is an intrinsic property of a command name and it never changed in run-time.
Command Flags:
Every command has its own flags, including alternate commands (flags are redundant in case of alternate commands). The alternate commands are an identical copy of their parent command except for their name. Therefore, the flags of an alternate command are initilized with that of its parent command but any changes made to the flags of the parent command wont cause any corresponding change in the flags of its alternate commands. For example, changing the flags of /spectate command does NOT alter the flags of its alternate /spec command.

These flags are essentially a tagged variable (tag of the variable is 'flags') which can store any data related to the command.

The command flags can be accessed and modified directly by referencing the flag variable. The flag variable's identifier is the command name itself prefixed with "flg_", for example, "flg_spectate".

In case you do not know the command name beforehand, you can use GetCommandFlags and SetCommandFlags to modify the flags of a command.

Command States/Modes:
SmartCMD uses the automata (state functions) feature provided in PAWN to implement command states/modes. The automata is named "cmdMode". The command function function without any mode is the default command which is executed every time a command is used unless explicitly mentioned. SmartCMD assigns the 'normal' state to the default command function.

You can change the command mode dynamically anywhere in the code using the following code,

Code:
state cmdMode:mode_name;
Whenever a command is executed, the state is set back to normal mode.

Performance Benchmarks
iZCMD Average (non-case sensitive): 1075ms
SmartCMD Average (non-case sensitive): 842ms

iZCMD Average (case-sensitive): 901ms
SmartCMD Average (case sensitive): 680ms

SmartCMD Benchmark Code: http://pastebin.com/x7tXAekb

Download

Github
Visit Github Repository

View the include

Credits
Yashas
****** - #define tricks
Crayder - constructive talks
Zeex - the concept of making public functions
Reply
#2

Awesome job! +rep.
Reply
#3

Very cool!
I'll be watching this along with that I'll start playing around with it tomorrow!
Reply
#4

*When all of existing command processor just not enough*
Reply
#5

Quote:
Originally Posted by RaeF
View Post
*When all of existing command processor just not enough*
There are few features in SmartCMD which are completely new and the design is completely different. I haven't seen a command processor which passes command ids to the callbacks and commands instead of the command name yet.
Reply
#6

I really apreciate your effort! The documentation is awesome.
Reply
#7

Amazing.
Reply
#8

Really nice!
Reply
#9

Good job! +REP.
Reply
#10

How to call a command?
Reply
#11

Pretty decent include .Thanks to you.
Reply
#12

Quote:
Originally Posted by eco1999
Посмотреть сообщение
How to call a command?
Код:
cmd_yourcommand(playerid, " "); // if you are using params
cmd_yourcommand(playerid); // if you are not using params
Reply
#13

Quote:
Originally Posted by SecretBoss
Посмотреть сообщение
Код:
cmd_yourcommand(playerid, " "); // if you are using params
cmd_yourcommand(playerid); // if you are not using params
No, i find.

native EmulateCommandEx(cmdid, playerid, params[])
native EmulateCommand(playerid, cmdtext[])
Reply
#14

Quote:
Originally Posted by eco1999
Посмотреть сообщение
No, i find.

native EmulateCommandEx(cmdid, playerid, params[])
native EmulateCommand(playerid, cmdtext[])
EmulateCommand will re-create the command, calling the command is much faster than recreating it.
Reply
#15

Any comparison between this and Pawn.CMD?

I mean this has tons of features but in terms of speed which is better?
Reply
#16

Maybe you can add ReassignCommandFunctionForPlayer?

This is very good, i have some issues with pawn.cmd i certainly will use
Reply
#17

Command declaration is what i found interesting. ☺
Reply
#18

The fight for the fastest command processor is starting to seem more competitive than the Trump vs Hilary campaign.


I'm just going to keep switching to the fastest regardless though and this seems pretty nice.
Reply
#19

Test code for SickAttack's include: http://pastebin.com/EJR7y49A

Results:
Quote:

[14:56:51] 1068
[14:56:52] 1078
[14:56:53] 1075
[14:56:54] 1070
[14:56:55] 1079
[14:56:56] 1071
[14:56:57] 1091
[14:56:58] 1063
[14:57:00] 1067
[14:57:01] 1062
[14:57:01] Average: 1072.40

SmartCMD test code: http://pastebin.com/ZWxA6p6a

Quote:

Results:
[14:57:48] 933
[14:57:49] 939
[14:57:50] 951
[14:57:51] 942
[14:57:52] 923
[14:57:53] 954
[14:57:54] 928
[14:57:55] 912
[14:57:55] 907
[14:57:56] 915
[14:57:56] Average: 930.40

Interesting, this include without dirty bloated code is faster than SickAttack's include. Moreover, I haven't added any performance improvement code to the callbacks so in principle SmartCMD is more faster than the timings shown in this test.
Reply
#20

Quote:
Originally Posted by Yashas
Посмотреть сообщение
Test code for SickAttack's include: http://pastebin.com/EJR7y49A

Results:


SmartCMD test code: http://pastebin.com/ZWxA6p6a

Results:
[14:57:48] 933
[14:57:49] 939
[14:57:50] 951
[14:57:51] 942
[14:57:52] 923
[14:57:53] 954
[14:57:54] 928
[14:57:55] 912
[14:57:55] 907
[14:57:56] 915
[14:57:56] Average: 930.40

Interesting, this include without dirty bloated code is faster than SickAttack's include. Moreover, I haven't added any performance improvement code to the callbacks so in principle SmartCMD is more faster than the timings shown in this test.
Nice, not sure if I was clear enough in my other post but I was going to switch over to this regardless if SickAttack's was faster or not. This just looks really nice and I see some useful stuff I can use.
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)