Talk:Introduction to SourceMod Plugins
--Asherkin (talk) 11:12, 17 March 2013 (CDT)
Creating client command
So far we have learned how to set up our plugin and run some code. But our plugin still doesn't do anything useful. Let's add a console command to allow clients to see their kills/deaths ratio. For demonstration purposes, we are going to start with very naive implementation that a beginner could write, then we'll look at the common bugs it contains, fix them and add more advanced features. In the end we'll have a mature feature-rich implementation and experience that will prevent us from making simple mistakes in the future.
Naive implementation
First, we need to register our console command. OnPluginStart is a good place to do that. In order to register our console command, we need to use RegConsoleCmd function, it is declared inside console.inc, here's how it's declared:
/** * Creates a console command, or hooks an already existing one. * * Console commands are case sensitive. However, if the command already exists in the game, * the a client may enter the command in any case. SourceMod corrects for this automatically, * and you should only hook the "real" version of the command. * * @param cmd Name of the command to hook or create. * @param callback A function to use as a callback for when the command is invoked. * @param description Optional description to use for command creation. * @param flags Optional flags to use for command creation. * @noreturn * @error Command name is the same as an existing convar. */ native RegConsoleCmd(const String:cmd[], ConCmd:callback, const String:description[]="", flags=0);
This native looks a bit more complicated than the previous one, so let's go through each argument step by step. First one is the name of our command, we'll use "sm_kdr". Second one is a callback, but it has unknown tag ConCmd. In order to correctly call RegConsoleCmd we need to find how ConCmd is declared. Thankfully, we don't need to look hard, it's declared right above RegConsoleCmd:
/** * Called when a generic console command is invoked. * * @param client Index of the client, or 0 from the server. * @param args Number of arguments that were in the argument string. * @return An Action value. Not handling the command * means that Source will report it as "not found." */ functag public Action:ConCmd(client, args);
This is a function tag. What does it mean? It means that we need to define our own function that will have the same prototype (number of arguments, their tags and return tag) as the one of function tag. However, in this case, we don't need to (and can't) use the same name as in tag, that would result in error. We need to use our own name, let's use Command_KDR. Function tags doesn't require to use the same argument names as specified in declaration, but it's a good idea to use them, because they usually have the most meaning. So, all in all, it leads us to the following definition:
public Action:Command_KDR(client, args) { }
Now, if you've been following carefully, you should see that we have another unknown tag here - Action. Let's look how it's declared (core.inc):
/** * Specifies what to do after a hook completes. */ enum Action { Plugin_Continue = 0, /**< Continue with the original action */ Plugin_Changed = 1, /**< Inputs or outputs have been overridden with new values */ Plugin_Handled = 3, /**< Handle the action at the end (don't call it) */ Plugin_Stop = 4, /**< Immediately stop the hook chain and handle the original */ };
It's an enumeration tag and in our callback it's up to us to decide which value to return. As you probably understood from reading all the info, SourceMod console commands are implemented as hooks between client and original server code. If we don't return anything and "let it slide" or return Plugin_Continue, the original command from client will reach the server and, since it's highly unlikely that there is sm_kdr command in the game, the message Unknown command "sm_kdr" will be printed to client's console. We don't want that, so let's modify our callback to return Plugin_Handled:
public Action:Command_KDR(client, args) { return Plugin_Handled; }
It's a very common mistake to forget to return Plugin_Handled so our naive implementation is going to be not so naive after all! Now, after we finally understood how ConCmd tag works, it's time to move to the next argument of RegConsoleCmd. It is a string that will act like a description of our command. However, notice the = after argument name, it indicates that this argument is optional, we can skip it and empty string ("") will be used. And the last argument is flags and it's also optional. Command flags are used to change behavior of command, but for now default behavior is ok. Now we know the meaning of all arguments, let's make a call.
RegConsoleCmd("sm_kdr", Command_KDR, "Displays the client their kills/deaths ratio.");
Notice that we skipped flags because we want default behavior. Now let's see how the full command-related code looks like:
public OnPluginStart() { RegConsoleCmd("sm_kdr", Command_KDR, "Displays the client their kills/deaths ratio."); } public Action:Command_KDR(client, args) { return Plugin_Handled; }
Why did we choose sm_kdr as the name of our command? It's handy because it will create convenient chat triggers. The skeleton of our command is ready, it is time to write the actual code for calculating KDR and displaying it to client. Notice that the first argument of our Command_KDR callback is client. When our callback is called, it will hold the index of client who called our command. We'll use this to find necessary information about client and to display that information back. First, let's find the number of kills, we'll use GetClientFrags function for that, it's declared in clients.inc:
/** * Returns the client's frag count. * * @param client Player's index. * @return Frag count. * @error Invalid client index, client not in game, or no mod support. */ native GetClientFrags(client);
It takes one argument - the client index and returns the number of frags. For the only argument of GetClientFrags we'll use client argument of our callback and we also need to store the return value so we'll create a local variable for that:
new kills = GetClientFrags(client);
By this time, it is assumed that you've learned how to read include files and find necessary information, so only links to online SourceMod API will be provided. Next, we need to find the number of client deaths, we'll use GetClientDeaths:
new deaths = GetClientDeaths(client);
Now that we have both kills and deaths, let's find the ratio. Remember that by default all variables in SourcePawn act like integers, but we need the fractional part in this case. Another very important thing to remember is that a result of integer division is also an integer. So we need to convert both kills and deaths to floating point values and store the result in a floating point variable. float function does the conversion.
new Float:ratio = float(kills) / float(deaths);
And now we only need to display the ratio, since our command can be called either via console or chat, it is good idea to display our info accordingly, so we'll use ReplyToCommand. It's another format class function and now we need to apply some formatting. In a nutshell, we pass the string which acts as a template for text and % sign and one-letter type identifier (which is called format specifier) for variable values to put inside that template. After that, we must pass the number of arguments that correspond to the number of format specifiers in our format string. Since we have a floating point number, the format specifier will be %f:
ReplyToCommand(client, "Your KDR is: %f.", ratio);
That's it! Now let's see the full code of our naive implementation:
#include <sourcemod> public Plugin:myinfo = { name = "My First Plugin", author = "Me", description = "My first plugin ever", version = "1.0", url = "http://www.sourcemod.net/" }; public OnPluginStart() { RegConsoleCmd("sm_kdr", Command_KDR, "Displays the client their kills/deaths ratio."); } public Action:Command_KDR(client, args) { new kills = GetClientFrags(client); new deaths = GetClientDeaths(client); new Float:ratio = float(kills) / float(deaths); ReplyToCommand(client, "Your KDR is: %f.", ratio); return Plugin_Handled; }
Compile and test this plugin and notice that something is wrong. We'll talk about it in the next section.