Difference between revisions of "Introduction to SourceMod Plugins"

From AlliedModders Wiki
Jump to: navigation, search
m (Client Callbacks)
m (Multiple Targets: Moved LoadTranslations below RegAdminCmd and added .txt)
(32 intermediate revisions by 16 users not shown)
Line 1: Line 1:
 
This guide will give you a basic introduction to writing a [[SourceMod]] plugin.  If you are not familiar with the SourcePawn language, it is recommended that you at least briefly read the [[Introduction to SourcePawn]] article.
 
This guide will give you a basic introduction to writing a [[SourceMod]] plugin.  If you are not familiar with the SourcePawn language, it is recommended that you at least briefly read the [[Introduction to SourcePawn]] article.
  
For information on compiling plugins, see [[Compiling SourceMod Plugins]]. You can use [http://www.crimsoneditor.com/ Crimson Editor], [http://www.pspad.com/ PSPad], [http://www.ultraedit.com/ UltraEdit], [http://notepad-plus.sourceforge.net/uk/site.htm Notepad++], [http://www.textpad.com/ TextPad], [http://sourceforge.net/projects/pawnstudio/ Pawn Studio] or any other text editor you're comfortable with to write plugins.
+
For information on compiling plugins, see [[Compiling SourceMod Plugins]]. You can use [https://forums.alliedmods.net/showthread.php?t=259917 SPEdit], [https://www.crimsoneditor.com/ Crimson Editor], [http://www.pspad.com/ PSPad], [http://www.ultraedit.com/ UltraEdit], [https://notepad-plus-plus.org/ Notepad++], [https://www.textpad.com/ TextPad], [http://sourceforge.net/projects/pawnstudio/ Pawn Studio], [https://forums.alliedmods.net/showthread.php?t=289127 BasicPawn] or any other text editor you're comfortable with to write plugins.
  
 
=Starting from scratch=
 
=Starting from scratch=
Open your favorite text editor and create a new empty file. When you have an empty file you can just start writing code using the core language, however, you will not be able to use any of SourceMod features because the compiler does not now about them. This is done deliberately so it is possible to use SourcePawn outside of SourceMod. But since we are writing a SourceMod plugin, it is a good idea to enable access to SourceMod features first. This it done using <tt>#include</tt> directive. It tells compiler to "paste" the code from another file into yours.
+
Open your favorite text editor and create a new empty file. When you have an empty file you can just start writing code using the core language, however, you will not be able to use any of SourceMod features because the compiler does not know about them. This is done deliberately so it is possible to use SourcePawn outside of SourceMod. But since we are writing a SourceMod plugin, it is a good idea to enable access to SourceMod features first. This is done using <tt>#include</tt> directive. It tells the compiler to "paste" the code from another file into yours.
 
<pawn>#include <sourcemod></pawn>
 
<pawn>#include <sourcemod></pawn>
How does this work? First of all, note that we enclosed file name into angle brackets. Angle brackets tell the compiler to look in the default include directory. By default, it is '''scripting/include'''. You can open it right now and see a lot of inc files there. Those are SourceMod include files that describe various functions, tags and other features available for SourceMod plugins. The files are plain-text and you are encouraged to read them. You will notice however, that there's not much code in there, certainly not enough to implement all the great features of SourceMod, so where are they? They are implemented inside a SourceMod core which is written in C++ and is compiled into binary files which end up in '''bin''' directory. So how does your SourcePawn code and SM core link together if compiler doesn't know about existence of the latter? SourceMod include files are written specially, so they say that the implementation of functions is ''somewhere else''. Compiler understands that and generate a special code that says that this function call is going outside. When SourceMod loads your plugin, it inspects these bits of code and substitutes it's own internal functions instead. This is called [http://en.wikipedia.org/wiki/Dynamic_linking dynamic linking].
+
How does this work? First of all, note that we enclosed file name into angle brackets. Angle brackets tell the compiler to look in the default include directory. By default, it is '''scripting/include'''. You can open it right now and see a lot of inc files there. Those are SourceMod include files that describe various functions, tags and other features available for SourceMod plugins. The files are plain-text and you are encouraged to read them. You will notice, however, that there's not much code in there, certainly not enough to implement all the great features of SourceMod, so where are they? They are implemented inside a SourceMod core which is written in C++ and is compiled into binary files which end up in '''bin''' directory. So how does your SourcePawn code and SM core link together if the compiler doesn't know about the existence of the latter? SourceMod include files are written specially, so they say that the implementation of functions is ''somewhere else''. The compiler understands that and generates a special code that says that this function call is going outside. When SourceMod loads your plugin, it inspects these bits of code and substitutes it's own internal functions instead. This is called [http://en.wikipedia.org/wiki/Dynamic_linking dynamic linking].
  
 
=Setting up plugin info=
 
=Setting up plugin info=
Now that we got access to SourceMod features, it is time to setup the information that will be displayed via <tt>sm plugins list</tt> command. No one likes unnamed plugins. To do that we are going to look inside '''sourcemod.inc''' file and see the format that information should be declared. It's always helpful to look inside SM include files to find out information you don't know. There is also an [http://docs.sourcemod.net/api/ API documentation] but it can be outdated and it only has SM core files so if your plugin are going to use any third party extension or another plugin, you will have to study inc files. So, open '''sourcemod.inc''' and scroll down a bit until you see this:
+
Now that we got access to SourceMod features, it is time to set up the information that will be displayed via <tt>sm plugins list</tt> command. No one likes unnamed plugins. To do that we are going to look inside '''sourcemod.inc''' file and see the format that information should be declared. It's always helpful to look inside SM include files to find out information you don't know. There is also an [http://docs.sourcemod.net/api/ API documentation] but it can be outdated and it only has SM core files so if your plugin is going to use any third party extension or another plugin, you will have to study inc files. So, open '''sourcemod.inc''' and scroll down a bit until you see this:
 
<pawn>/**
 
<pawn>/**
 
  * Plugin public information.
 
  * Plugin public information.
Line 15: Line 15:
 
struct Plugin
 
struct Plugin
 
{
 
{
   const String:name[], /**< Plugin Name */
+
   public const char[] name; /**< Plugin Name */
   const String:description[], /**< Plugin Description */
+
   public const char[] description; /**< Plugin Description */
   const String:author[], /**< Plugin Author */
+
   public const char[] author; /**< Plugin Author */
   const String:version[], /**< Plugin Version */
+
   public const char[] version; /**< Plugin Version */
   const String:url[], /**< Plugin URL */
+
   public const char[] url; /**< Plugin URL */
 
};</pawn>
 
};</pawn>
 
and this:
 
and this:
Line 26: Line 26:
 
  * Example:
 
  * Example:
 
  *
 
  *
  * public Plugin:myinfo =
+
  * public Plugin myinfo =
 
  * {
 
  * {
 
  *    name = "My Plugin",
 
  *    name = "My Plugin",
Line 32: Line 32:
 
  * };
 
  * };
 
  */
 
  */
public Plugin:myinfo;</pawn>
+
public Plugin myinfo;</pawn>
  
 
It tells us that we need to create a global public variable <tt>myinfo</tt> which must be of type <tt>Plugin</tt> which is a struct with 5 fields which themselves are strings. It may sound complicated for a beginner but it's easy. Let's go ahead and create one:
 
It tells us that we need to create a global public variable <tt>myinfo</tt> which must be of type <tt>Plugin</tt> which is a struct with 5 fields which themselves are strings. It may sound complicated for a beginner but it's easy. Let's go ahead and create one:
<pawn>public Plugin:myinfo =
+
<pawn>public Plugin myinfo =
 
{
 
{
 
name = "My First Plugin",
 
name = "My First Plugin",
Line 44: Line 44:
 
};</pawn>
 
};</pawn>
  
The <tt>public</tt> keyword means that SourceMod will be able to directly access our variable. <tt>Plugin:</tt> defines a type of our variable. <tt>myinfo</tt> is, obviously, a name of our variable as required by SourceMod. You see that we initialize it right away. This is preferred way to do when filling out plugin info.
+
The <tt>public</tt> keyword means that SourceMod will be able to directly access our variable. <tt>Plugin:</tt> defines a type of our variable. <tt>myinfo</tt> is, obviously, a name of our variable as required by SourceMod. You see that we initialize it right away. This is the preferable way to fill out plugin info.
  
 
After that the full code of your plugin should look like this:
 
After that the full code of your plugin should look like this:
 
<pawn>#include <sourcemod>
 
<pawn>#include <sourcemod>
  
public Plugin:myinfo =
+
public Plugin myinfo =
 
{
 
{
 
name = "My First Plugin",
 
name = "My First Plugin",
Line 59: Line 59:
  
 
=Getting code to run=
 
=Getting code to run=
We already include SourceMod features and filled up or plugin info. We now have a perfectly well formed plugin which can be compiled and loaded by SourceMod. However, there is one problem - it does nothing. You might be tempted to just start writing a code after <tt>myinfo</tt> declaration just to see that it will not compile. SourcePawn, unlike other scripting languages like Lua, does not allow a code to be outside of functions. After reading that, you may probably want to just define some function, name it <tt>main</tt> probably, compile and load a plugin and see that your code never gets called. So how do we make SourceMod call our code? For this exact reason we have forwards. Forwards are function prototypes declared by one party that can be implemented by another party as a [http://en.wikipedia.org/wiki/Callback_%28computer_programming%29 callback]. When a first party starts a forward call, all parties that have matching callbacks receive the call. SourceMod declares a plenty of interesting forwards that we can implement. As you can see, forwards are the only way to get our code executed, keep that in mind. So let's implement <tt>OnPluginStart</tt> forward. As you may have guessed, it is called when our plugin starts. To do that, we'll have to look up the declaration of <tt>OnPluginStart</tt>. It is declared inside '''sourcemod.inc''', a file we are already familiar with, let's find it:
+
We already include SourceMod features and filled up or plugin info. We now have a perfectly well-formed plugin which can be compiled and loaded by SourceMod. However, there is one problem - it does nothing. You might be tempted to just start writing a code after <tt>myinfo</tt> declaration just to see that it will not compile. SourcePawn, unlike other scripting languages like Lua, does not allow a code to be outside of functions. After reading that, you may probably want to just define some function, name it <tt>main</tt> probably, compile and load a plugin and see that your code never gets called. So how do we make SourceMod call our code? For this exact reason, we have forwards. Forwards are function prototypes declared by one party that can be implemented by another party as a [http://en.wikipedia.org/wiki/Callback_%28computer_programming%29 callback]. When a first party starts a forward call, all parties that have matching callbacks receive the call. SourceMod declares a plenty of interesting forwards that we can implement. As you can see, forwards are the only way to get our code executed, keep that in mind. So let's implement <tt>OnPluginStart</tt> forward. As you may have guessed, it is called when our plugin starts. To do that, we'll have to look up the declaration of <tt>OnPluginStart</tt>. It is declared inside '''sourcemod.inc''', a file we are already familiar with, let's find it:
 
<pawn>/**
 
<pawn>/**
 
  * Called when the plugin is fully initialized and all known external references  
 
  * Called when the plugin is fully initialized and all known external references  
Line 74: Line 74:
 
  * @noreturn
 
  * @noreturn
 
  */
 
  */
forward OnPluginStart();</pawn>
+
forward void OnPluginStart();</pawn>
Empty parentheses tells us that no arguments are passed inside this forward, <tt>@noreturn</tt> inside documentation tells us that we don't have to return anything, pretty simple forward. So how to write a correct callback for it? Firstly, our callback must have the same name, so it's <tt>OnPluginStart</tt>, secondly, our callback should have the same number of arguments, none in this case, and lastly, SourceMod needs to be able to call our callback so it needs to be <tt>public</tt>. So the implementation looks like this:
+
Empty parentheses tell us that no arguments are passed inside this forward, <tt>@noreturn</tt> inside documentation tells us that we don't have to return anything, pretty simple forward. So how to write a correct callback for it? Firstly, our callback must have the same name, so it's <tt>OnPluginStart</tt>, secondly, our callback should have the same number of arguments, none in this case, and lastly, SourceMod needs to be able to call our callback so it needs to be <tt>public</tt>. So the implementation looks like this:
<pawn>public OnPluginStart()
+
<pawn>public void OnPluginStart()
 
{
 
{
 
}</pawn>
 
}</pawn>
Line 88: Line 88:
 
  * @noreturn
 
  * @noreturn
 
  */
 
  */
native PrintToServer(const String:format[], any:...);</pawn>
+
native int PrintToServer(const char[] format, any ...);</pawn>
 
As you can see, this is a native function. It is implemented inside SM core. Judging by it's arguments, we can see that it is a [[Format_Class_Functions_%28SourceMod_Scripting%29|format class function]]. However, we don't need any formatting right now, so let's just pass <tt>"Hello world!"</tt> string as an only argument:
 
As you can see, this is a native function. It is implemented inside SM core. Judging by it's arguments, we can see that it is a [[Format_Class_Functions_%28SourceMod_Scripting%29|format class function]]. However, we don't need any formatting right now, so let's just pass <tt>"Hello world!"</tt> string as an only argument:
<pawn>public OnPluginStart()
+
<pawn>public void OnPluginStart()
 
{
 
{
 
PrintToServer("Hello world!");
 
PrintToServer("Hello world!");
Line 97: Line 97:
 
<pawn>#include <sourcemod>
 
<pawn>#include <sourcemod>
  
public Plugin:myinfo =
+
public Plugin myinfo =
 
{
 
{
 
name = "My First Plugin",
 
name = "My First Plugin",
Line 106: Line 106:
 
};
 
};
  
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
 
PrintToServer("Hello world!");
 
PrintToServer("Hello world!");
 
}</pawn>
 
}</pawn>
Compile and load your plugin on your server and see for yourself that the message is displayed in server console.
+
Compile and load your plugin on your server and see for yourself that the message is displayed in the server console.
 
 
=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. <tt>OnPluginStart</tt> is a good place to do that. In order to register our console command, we need to use <tt>RegConsoleCmd</tt> function, it is declared inside '''console.inc''', here's how it's declared:
 
<pawn>/**
 
* 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);</pawn>
 
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 <tt>"sm_kdr"</tt>. Second one is a callback, but it has unknown tag <tt>ConCmd</tt>. In order to correctly call <tt>RegConsoleCmd</tt> we need to find how <tt>ConCmd</tt> is declared. Thankfully, we don't need to look hard, it's declared right above <tt>RegConsoleCmd</tt>:
 
<pawn>/**
 
* 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);</pawn>
 
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 <tt>Command_KDR</tt>. 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:
 
<pawn>public Action:Command_KDR(client, args)
 
{
 
}</pawn>
 
Now, if you've been following carefully, you should see that we have another unknown tag here - <tt>Action</tt>. Let's look how it's declared ('''core.inc'''):
 
<pawn>/**
 
* 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 */
 
};</pawn>
 
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 <tt>Plugin_Continue</tt>, the original command from client will reach the server and, since it's highly unlikely that there is <tt>sm_kdr</tt> command in the game, the message <tt>Unknown command "sm_kdr"</tt> will be printed to client's console. We don't want that, so let's modify our callback to return <tt>Plugin_Handled</tt>:
 
<pawn>public Action:Command_KDR(client, args)
 
{
 
return Plugin_Handled;
 
}</pawn>
 
It's a very common mistake to forget to return <tt>Plugin_Handled</tt> so our naive implementation is going to be not so naive after all! Now, after we finally understood how <tt>ConCmd</tt> tag works, it's time to move to the next argument of <tt>RegConsoleCmd</tt>. It is a string that will act like a description of our command. However, notice the <tt>=</tt> after argument name, it indicates that this argument is optional, we can skip it and empty string (<tt>""</tt>) 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.
 
<pawn>RegConsoleCmd("sm_kdr", Command_KDR, "Displays the client their kills/deaths ratio.");</pawn>
 
Notice that we skipped flags because we want default behavior. Now let's see how the full command-related code looks like:
 
<pawn>public OnPluginStart()
 
{
 
RegConsoleCmd("sm_kdr", Command_KDR, "Displays the client their kills/deaths ratio.");
 
}
 
 
 
public Action:Command_KDR(client, args)
 
{
 
return Plugin_Handled;
 
}</pawn>
 
Why did we choose <tt>sm_kdr</tt> as the name of our command? It's handy because it will create convenient [[Commands_%28SourceMod_Scripting%29#Chat_Triggers|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 <tt>Command_KDR</tt> callback is <tt>client</tt>. 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 <tt>GetClientFrags</tt> function for that, it's declared in '''clients.inc''':
 
<pawn>/**
 
* 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);</pawn>
 
It takes one argument - the client index and returns the number of frags. For the only argument of <tt>GetClientFrags</tt> we'll use <tt>client</tt> argument of our callback and we also need to store the return value so we'll create a local variable for that:
 
<pawn>new kills = GetClientFrags(client);</pawn>
 
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 <tt>[http://docs.sourcemod.net/api/index.php?fastload=show&id=431& GetClientDeaths]</tt>:
 
<pawn>new deaths = GetClientDeaths(client);</pawn>
 
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. <tt>[http://docs.sourcemod.net/api/index.php?fastload=show&id=705& float]</tt> function does the conversion.
 
<pawn>new Float:ratio = float(kills) / float(deaths);</pawn>
 
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 <tt>[http://docs.sourcemod.net/api/index.php?fastload=show&id=462& ReplyToCommand]</tt>. It's another [[Format_Class_Functions_(SourceMod_Scripting)|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 <tt>%</tt> 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 <tt>%f</tt>:
 
<pawn>ReplyToCommand(client, "Your KDR is: %f.", ratio);</pawn>
 
That's it! Now let's see the full code of our naive implementation:
 
<pawn>#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;
 
}</pawn>
 
Compile and test this plugin and notice that something is wrong. We'll talk about it in the next section.
 
  
 
=Includes=
 
=Includes=
Line 230: Line 124:
  
 
==Declaration==
 
==Declaration==
First, let's look at what an admin command requires.  Admin commands are registered using the [http://docs.sourcemod.net/api/index.php?fastload=show&id=471& RegAdminCmd] function.  They require a '''name''', a '''callback function''', and '''default admin flags'''.   
+
First, let's look at what an admin command requires.  Admin commands are registered using the [https://sm.alliedmods.net/new-api/console/RegAdminCmd RegAdminCmd] function.  They require a '''name''', a '''callback function''', and '''default admin flags'''.   
  
The callback function is what's invoked every time the command is used.  [http://docs.sourcemod.net/api/index.php?fastload=show&id=469& Click here] to see its prototype.  Example:
+
The callback function is what's invoked every time the command is used.  [https://sm.alliedmods.net/new-api/console/ConCmd Click here] to see its prototype.  Example:
  
 
<pawn>
 
<pawn>
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 
}
 
}
  
public Action:Command_MySlap(client, args)
+
public Action Command_MySlap(int client, int args)
 
{
 
{
 
}</pawn>
 
}</pawn>
  
Now we've successfully implemented a command -- though it doesn't do anything yet.  In fact, it will say "Unknown command" if you use it!  The reason is because of the <tt>Action</tt> tag.  Any command that you type in the console, even if it's registered by SourceMod, will be sent to the engine to be processed. Because we have not had SourceMod block this functionality yet, the engine replies with "Unknown command" because it is not a valid engine command.
+
Now we've successfully implemented a command -- though it doesn't do anything yet.  In fact, it will say "Unknown command" if you use it! This is because you're not returning Plugin_Handled in your callback. Since you haven't, SourceMod believes you didn't want the Source Engine to know the command was registered, and it handles it so. The reason SourceMod expects your function to return Plugin_Handled is because of the Action tag you put in your function's prototype. The Action tag specifies that Command_MySlap must return one of four things. See the [https://sm.alliedmods.net/new-api/core/Action Action] enumeration in the sourcemod API to learn more about these return types and when to use them.
  
<pawn>public Action:Command_MySlap(client, args)
+
<pawn>public Action Command_MySlap(int client, int args)
 
{
 
{
 
return Plugin_Handled;
 
return Plugin_Handled;
Line 258: Line 152:
  
 
To implement this, we'll need a few steps:
 
To implement this, we'll need a few steps:
*Get the input from the console.  For this we use [http://docs.sourcemod.net/api/index.php?fastload=show&id=473& GetCmdArg()].
+
*Get the input from the console.  For this we use [https://sm.alliedmods.net/new-api/console/GetCmdArg GetCmdArg()].
*Find a matching player.  For this we use [http://docs.sourcemod.net/api/index.php?fastload=show&id=144& FindTarget()].
+
*Find a matching player.  For this we use [https://sm.alliedmods.net/new-api/helpers/FindTarget FindTarget()].
*Slap them.  For this we use [http://docs.sourcemod.net/api/index.php?fastload=show&id=42& SlapPlayer()], which requires including <tt>sdktools</tt>, an extension bundled with SourceMod.
+
*Slap them.  For this we use [https://sm.alliedmods.net/new-api/sdktools_functions/SlapPlayer SlapPlayer()], which requires including <tt>sdktools</tt>, an extension bundled with SourceMod.
*Respond to the admin.  For this we use [http://docs.sourcemod.net/api/index.php?fastload=show&id=462& ReplyToCommand()].
+
*Respond to the admin.  For this we use [https://sm.alliedmods.net/new-api/console/ReplyToCommand ReplyToCommand()].
  
 
Full example:
 
Full example:
Line 269: Line 163:
 
#include <sdktools>
 
#include <sdktools>
  
public Plugin:myinfo =
+
public Plugin myinfo =
 
{
 
{
 
name = "My First Plugin",
 
name = "My First Plugin",
Line 278: Line 172:
 
}
 
}
  
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 +
LoadTranslations("common.phrases.txt"); // Required for FindTarget fail reply
 
}
 
}
  
public Action:Command_MySlap(client, args)
+
public Action Command_MySlap(int client, int args)
 
{
 
{
new String:arg1[32], String:arg2[32];
+
char arg1[32], arg2[32];
new damage;
+
 +
/* By default, we set damage = 0 */
 +
int damage = 0;
  
 
/* Get the first argument */
 
/* Get the first argument */
 
GetCmdArg(1, arg1, sizeof(arg1));
 
GetCmdArg(1, arg1, sizeof(arg1));
  
/* If there are 2 or more arguments, and the second argument fetch
+
/* If there are 2 or more arguments, we set damage to
* is successful, convert it to an integer.
+
* what the user specified. If a damage isn't specified
*/
+
* then it will stay zero. */
if (args >= 2 && GetCmdArg(2, arg2, sizeof(arg2)))
+
if (args >= 2)
 
{
 
{
 +
GetCmdArg(2, arg2, sizeof(arg2));
 
damage = StringToInt(arg2);
 
damage = StringToInt(arg2);
 
}
 
}
  
 
/* Try and find a matching player */
 
/* Try and find a matching player */
new target = FindTarget(client, arg1);
+
int target = FindTarget(client, arg1);
 
if (target == -1)
 
if (target == -1)
 
{
 
{
 
/* FindTarget() automatically replies with the  
 
/* FindTarget() automatically replies with the  
* failure reason.
+
* failure reason and returns -1 so we know not
 +
* to continue
 
*/
 
*/
 
return Plugin_Handled;
 
return Plugin_Handled;
Line 311: Line 210:
 
SlapPlayer(target, damage);
 
SlapPlayer(target, damage);
  
new String:name[MAX_NAME_LENGTH];
+
char name[MAX_NAME_LENGTH];
 
 
 
GetClientName(target, name, sizeof(name));
 
GetClientName(target, name, sizeof(name));
Line 324: Line 223:
 
ConVars, also known as cvars, are global console variables in the Source engine.  They can have integer, float, or string values.  ConVar accessing is done through [[Handles (SourceMod Scripting)|Handles]].  Since ConVars are global, you do not need to close ConVar Handles (in fact, you cannot).
 
ConVars, also known as cvars, are global console variables in the Source engine.  They can have integer, float, or string values.  ConVar accessing is done through [[Handles (SourceMod Scripting)|Handles]].  Since ConVars are global, you do not need to close ConVar Handles (in fact, you cannot).
  
The handy feature of ConVars is that they are easy for users to configure.  They can be placed in any .cfg file, such as <tt>server.cfg</tt> or <tt>sourcemod.cfg</tt>.  To make this easier, SourceMod has an [http://docs.sourcemod.net/api/index.php?fastload=show&id=607& AutoExecConfig()] function.  This function will automatically build a default .cfg file containing all of your cvars, annotated with comments, for users.  It is highly recommend that you call this if you have customizable ConVars.
+
The handy feature of ConVars is that they are easy for users to configure.  They can be placed in any .cfg file, such as <tt>server.cfg</tt> or <tt>sourcemod.cfg</tt>.  To make this easier, SourceMod has an [https://sm.alliedmods.net/new-api/sourcemod/AutoExecConfig AutoExecConfig()] function.  This function will automatically build a default .cfg file containing all of your cvars, annotated with comments, for users.  It is highly recommended that you call this if you have customizable ConVars.
  
Let's extend your example from earlier with a new ConVar.  Our ConVar will be <tt>sm_myslap_damage</tt> and will specify the default damage someone is slapped for if no damage is specified.
+
Let's extend your example from earlier with a new ConVar.  Our ConVar will be <tt>g_cvarMySlapDamage</tt> and will specify the default damage someone is slapped for if no damage is specified.
  
<pawn>new Handle:sm_myslap_damage = INVALID_HANDLE
+
<pawn>ConVar g_cvarMySlapDamage = null;
  
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
  
sm_myslap_damage = CreateConVar("sm_myslap_damage", "5", "Default slap damage");
+
g_cvarMySlapDamage = CreateConVar("sm_myslap_damage", "5", "Default slap damage");
 
AutoExecConfig(true, "plugin_myslap");
 
AutoExecConfig(true, "plugin_myslap");
 
}
 
}
  
public Action:Command_MySlap(client, args)
+
public Action Command_MySlap(int client, int args)
 
{
 
{
new String:arg1[32], String:arg2[32];
+
char arg1[32], arg2[32];
new damage = GetConVarInt(sm_myslap_damage);
+
int damage = g_cvarMySlapDamage.IntValue;
  
 
/* The rest remains unchanged! */
 
/* The rest remains unchanged! */
Line 347: Line 246:
  
 
=Showing Activity, Logging=
 
=Showing Activity, Logging=
Almost all admin commands should log their activity, and some admin commands should show their activity to in-game clients.  This can be done via the [http://docs.sourcemod.net/api/index.php?fastload=show&id=599& LogAction()] and [http://docs.sourcemod.net/api/index.php?fastload=show&id=466& ShowActivity2()] functions.  The exact functionality of ShowActivity2() is determined by the <tt>sm_show_activity</tt> cvar.
+
Almost all admin commands should log their activity, and some admin commands should show their activity to in-game clients.  This can be done via the [https://sm.alliedmods.net/new-api/logging/LogAction LogAction()] and [https://sm.alliedmods.net/new-api/console/ShowActivity2 ShowActivity2()] functions.  The exact functionality of ShowActivity2() is determined by the <tt>sm_show_activity</tt> cvar.
  
 
For example, let's rewrite the last few lines of our slap command:
 
For example, let's rewrite the last few lines of our slap command:
Line 353: Line 252:
 
SlapPlayer(target, damage);
 
SlapPlayer(target, damage);
  
new String:name[MAX_NAME_LENGTH];
+
char name[MAX_NAME_LENGTH];
 
 
 
GetClientName(target, name, sizeof(name));
 
GetClientName(target, name, sizeof(name));
Line 367: Line 266:
 
To fully complete our slap demonstration, let's make it support multiple targets.  SourceMod's [[Admin_Commands_%28SourceMod%29#How_to_Target|targeting system]] is quite advanced, so using it may seem complicated at first.   
 
To fully complete our slap demonstration, let's make it support multiple targets.  SourceMod's [[Admin_Commands_%28SourceMod%29#How_to_Target|targeting system]] is quite advanced, so using it may seem complicated at first.   
  
The function we use is [http://docs.sourcemod.net/api/index.php?fastload=show&id=703& ProcessTargetString()].  It takes in input from the console, and returns a list of matching clients.  It also returns a noun that will identify either a single client or describe a list of clients.  The idea is that each client is then processed, but the activity shown to all players is only processed once.  This reduces screen spam.
+
The function we use is [https://sm.alliedmods.net/new-api/commandfilters/ProcessTargetString ProcessTargetString()].  It takes in input from the console and returns a list of matching clients.  It also returns a noun that will identify either a single client or describe a list of clients.  The idea is that each client is then processed, but the activity shown to all players is only processed once.  This reduces screen spam.
  
This method of target processing is used for almost every admin command in SourceMod, and in fact FindTarget() is just a simplified version.
+
This method of target processing is used for almost every admin command in SourceMod, and in fact, FindTarget() is just a simplified version.
  
 
Full, final example:
 
Full, final example:
Line 376: Line 275:
 
#include <sdktools>
 
#include <sdktools>
  
new Handle:sm_myslap_damage = INVALID_HANDLE
+
ConVar g_cvarMySlapDamage = null;
  
public Plugin:myinfo =
+
public Plugin myinfo =
 
{
 
{
 
name = "My First Plugin",
 
name = "My First Plugin",
Line 387: Line 286:
 
}
 
}
  
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
LoadTranslations("common.phrases");
 
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 
RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 +
LoadTranslations("common.phrases.txt");
  
sm_myslap_damage = CreateConVar("sm_myslap_damage", "5", "Default slap damage");
+
g_cvarMySlapDamage = CreateConVar("sm_myslap_damage", "5", "Default slap damage");
 
AutoExecConfig(true, "plugin_myslap");
 
AutoExecConfig(true, "plugin_myslap");
 
}
 
}
  
public Action:Command_MySlap(client, args)
+
public Action Command_MySlap(int client, int args)
 
{
 
{
new String:arg1[32], String:arg2[32];
+
char arg1[32], arg2[32];
new damage = GetConVarInt(sm_myslap_damage);
+
int damage = g_cvarMySlapDamage.IntValue;
  
 
/* Get the first argument */
 
/* Get the first argument */
Line 418: Line 317:
 
* tn_is_ml - stores whether the noun must be translated
 
* tn_is_ml - stores whether the noun must be translated
 
*/
 
*/
new String:target_name[MAX_TARGET_LENGTH];
+
char target_name[MAX_TARGET_LENGTH];
new target_list[MAXPLAYERS], target_count;
+
int target_list[MAXPLAYERS], target_count;
new bool:tn_is_ml;
+
bool tn_is_ml;
  
 
if ((target_count = ProcessTargetString(
 
if ((target_count = ProcessTargetString(
Line 437: Line 336:
 
}
 
}
  
for (new i = 0; i < target_count; i++)
+
for (int i = 0; i < target_count; i++)
 
{
 
{
 
SlapPlayer(target_list[i], damage);
 
SlapPlayer(target_list[i], damage);
Line 454: Line 353:
 
return Plugin_Handled;
 
return Plugin_Handled;
 
}</pawn>
 
}</pawn>
 
=Client and Entity Indexes=
 
One major point of confusion with Half-Life 2 is the difference between the following things:
 
*Client index
 
*Entity index
 
*Userid
 
 
The first answer is that clients are entities.  Thus, a client index and an entity index are the same thing.  When a SourceMod function asks for an entity index, a client index can be specified.  When a SourceMod function asks for a client index, usually it means only a client index can be specified.
 
 
A fast way to check if an entity index is a client is checking whether it's between 1 and [http://docs.sourcemod.net/api/index.php?fastload=show&id=397& GetMaxClients()] (inclusive).  If a server has N client slots maximum, then entities 1 through N are always reserved for clients.  Note that 0 is a valid entity index; it is the world entity (worldspawn).
 
 
A userid, on the other hand, is completely different.  The server maintains a global "connection count" number, and it starts at 1.  Each time a client connects, the connection count is incremented, and the client receives that new number as their userid.
 
 
For example, the first client to connect has a userid of 2.  If he exits and rejoins, his userid will be 3 (unless another client joins in-between).  Since clients are disconnected on mapchange, their userids change as well.  Userids are a handy way to check if a client's connection status has changed.
 
 
SourceMod provides two functions for userids: [http://docs.sourcemod.net/api/index.php?fastload=show&id=442& GetClientOfUserId()] and [http://docs.sourcemod.net/api/index.php?fastload=show&id=402& GetClientUserId()].
 
  
 
=Events=
 
=Events=
Line 477: Line 360:
 
*They are almost always informational.  That is, blocking <tt>player_death</tt> will not stop a player from dying.  It may block a HUD or console message or something else minor.
 
*They are almost always informational.  That is, blocking <tt>player_death</tt> will not stop a player from dying.  It may block a HUD or console message or something else minor.
 
*They almost always use userids instead of client indexes.
 
*They almost always use userids instead of client indexes.
*Just because it is in a resource file does not mean it is ever called, or works the way you expect it to.  Mods are notorious at not properly documenting their event functionality.
+
*Just because it is in a resource file does not mean it is ever called, or works the way you expect it to.  Mods are notorious for not properly documenting their event functionality.
  
 
An example of finding when a player dies:
 
An example of finding when a player dies:
 
<pawn>
 
<pawn>
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
 
   HookEvent("player_death", Event_PlayerDeath);
 
   HookEvent("player_death", Event_PlayerDeath);
 
}
 
}
  
public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast)
+
public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
 
{
 
{
   new victim_id = GetEventInt(event, "userid");
+
   int victim_id = event.GetInt("userid");
   new attacker_id = GetEventInt(event, "attacker");
+
   int attacker_id = event.GetInt("attacker");
  
   new victim = GetClientOfUserId(victim_id);
+
   int victim = GetClientOfUserId(victim_id);
   new attacker = GetClientOfUserId(attacker_id);
+
   int attacker = GetClientOfUserId(attacker_id);
  
 
   /* CODE */
 
   /* CODE */
Line 498: Line 381:
  
 
=Callback Orders and Pairing=
 
=Callback Orders and Pairing=
SourceMod has a number of builtin callbacks about the state of the server and plugin.  Some of these are paired in special ways which is confusing to users.
+
SourceMod has a number of builtin callbacks about the state of the server and plugin.  Some of these are paired in special ways which can confuse users.
  
 
==Pairing==
 
==Pairing==
 
'''Pairing''' is SourceMod terminology.  Examples of it are:
 
'''Pairing''' is SourceMod terminology.  Examples of it are:
 
*OnMapEnd() cannot be called without an OnMapStart(), and if OnMapStart() is called, it cannot be called again without an OnMapEnd().
 
*OnMapEnd() cannot be called without an OnMapStart(), and if OnMapStart() is called, it cannot be called again without an OnMapEnd().
*OnClientConnected(N) for a given client N will only be called once, until an OnClientDisconnected(N) for the same client N is called (which is guaranteed to happen).
+
*OnClientConnected(N) for a given client N will only be called once until an OnClientDisconnected(N) for the same client N is called (which is guaranteed to happen).
  
 
There is a formal definition of SourceMod's pairing.  For two functions X and Y, both with input A, the following conditions hold:
 
There is a formal definition of SourceMod's pairing.  For two functions X and Y, both with input A, the following conditions hold:
Line 514: Line 397:
 
These callbacks are listed in the order they are called, in the lifetime of a plugin and the server.
 
These callbacks are listed in the order they are called, in the lifetime of a plugin and the server.
  
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=940& AskPluginLoad2()] - Called once, immediately after the plugin is loaded from the disk.  This function can be used to stop a plugin from loading and return a custom error message; return APLRes_Failure and use strcopy on to replace the error string.  All CreateNative and RegPluginLibrary calls should be done here.   
+
*[https://sm.alliedmods.net/new-api/sourcemod/AskPluginLoad2 AskPluginLoad2()] - Called once, immediately after the plugin is loaded from the disk.  This function can be used to stop a plugin from loading and return a custom error message; return APLRes_Failure and use strcopy on to replace the error string.  All CreateNative and RegPluginLibrary calls should be done here.   
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=575& OnPluginStart()] - Called once, after the plugin has been fully initialized and can proceed to load.  Any run-time errors in this function will cause the plugin to fail to load.  '''This is paired with OnPluginEnd()'''.
+
*[https://sm.alliedmods.net/new-api/sourcemod/OnPluginStart OnPluginStart()] - Called once, after the plugin has been fully initialized and can proceed to load.  Any run-time errors in this function will cause the plugin to fail to load.  '''This is paired with OnPluginEnd()'''.
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=583& OnAllPluginsLoaded()] - Called once, after all non-late loaded plugins have called OnPluginStart.   
+
*[https://sm.alliedmods.net/new-api/sourcemod/OnAllPluginsLoaded OnAllPluginsLoaded()] - Called once, after all non-late loaded plugins have called OnPluginStart.   
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=580& OnMapStart()] - Called every time the map loads.  If the plugin is loaded late, and the map has already started, this function is called anyway after load, in order to preserve pairing.  '''This function is paired with OnMapEnd().'''
+
*[https://sm.alliedmods.net/new-api/sourcemod/OnMapStart OnMapStart()] - Called every time the map loads.  If the plugin is loaded late, and the map has already started, this function is called anyway after load, in order to preserve pairing.  '''This function is paired with OnMapEnd().'''
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=582& OnConfigsExecuted()] - Called once per map-change after  <tt>servercfgfile</tt> (usually <tt>server.cfg</tt>), <tt>sourcemod.cfg</tt>, and all plugin config files have finished executing.  If a plugin is loaded after this has happened, the callback is called anyway, in order to preserve pairing.  '''This function is paired with OnMapEnd().'''
+
*[https://sm.alliedmods.net/new-api/sourcemod/OnConfigsExecuted OnConfigsExecuted()] - Called once per map-change after  <tt>servercfgfile</tt> (usually <tt>server.cfg</tt>), <tt>sourcemod.cfg</tt>, and all plugin config files have finished executing.  If a plugin is loaded after this has happened, the callback is called anyway, in order to preserve pairing.  '''This function is paired with OnMapEnd().'''
 
*At this point, most game callbacks can occur, such as events and callbacks involving clients (or other things, like OnGameFrame).
 
*At this point, most game callbacks can occur, such as events and callbacks involving clients (or other things, like OnGameFrame).
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=581& OnMapEnd()] - Called when the map is about to end.  At this point, all clients are disconnected, but <tt>TIMER_NO_MAPCHANGE</tt> timers are not yet destroyed.  '''This function is paired to OnMapStart().'''
+
*[https://sm.alliedmods.net/new-api/sourcemod/OnMapEnd OnMapEnd()] - Called when the map is about to end.  At this point, all clients are disconnected, but <tt>TIMER_NO_MAPCHANGE</tt> timers are not yet destroyed.  '''This function is paired to OnMapStart().'''
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=577& OnPluginEnd()] - Called once, immediately before the plugin is unloaded.  '''This function is paired to OnPluginStart().'''
+
*[https://sm.alliedmods.net/new-api/sourcemod/OnPluginEnd OnPluginEnd()] - Called once, immediately before the plugin is unloaded.  '''This function is paired to OnPluginStart().'''
  
 
==Client Callbacks==
 
==Client Callbacks==
 
These callbacks are listed in no specific order, however, their documentation holds for both fake and real clients.
 
These callbacks are listed in no specific order, however, their documentation holds for both fake and real clients.
  
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=388& OnClientConnect()] - Called when a player initiates a connection.  You can block a player from connecting by returning Plugin_Stop and setting rejectmsg to an error message. '''This is paired with OnClientDisconnect() for successful connections only.'''
+
*[https://sm.alliedmods.net/new-api/clients/OnClientConnect OnClientConnect()] - Called when a player initiates a connection.  You can block a player from connecting by returning Plugin_Stop and setting rejectmsg to an error message.
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=916& OnClientConnected()] - Called after a player connects. Signifies that the player is in-game and IsClientConnected() will return true. '''This is paired with OnClientDisconnect() for successful connections only.'''
+
*[https://sm.alliedmods.net/new-api/clients/OnClientConnected OnClientConnected()] - Called after a player connects. Signifies that the player is in-game and IsClientConnected() will return true. '''This is paired with OnClientDisconnect() for successful connections only.'''
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=394& OnClientAuthorized()] - Called when a player gets a Steam ID.  It is important to note that this may never be called.  It may occur any time in between OnClientConnected and OnClientPreAdminCheck/OnClientDisconnect.  Do not rely on it unless you are writing something that needs Steam IDs, and even then you should use OnClientPostAdminCheck().
+
*[https://sm.alliedmods.net/new-api/clients/OnClientAuthorized OnClientAuthorized()] - Called when a player gets a Steam ID.  It is important to note that this may never be called.  It may occur any time in between OnClientConnected and OnClientPreAdminCheck/OnClientDisconnect.  Do not rely on it unless you are writing something that needs Steam IDs, and even then you should use OnClientPostAdminCheck().
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=389& OnClientPutInServer()] - Signifies that the player is in-game and IsClientInGame() will return true.
+
*[https://sm.alliedmods.net/new-api/clients/OnClientPutInServer OnClientPutInServer()] - Signifies that the player is in-game and IsClientInGame() will return true.
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=396& OnClientPreAdminCheck()] - Called after the player is '''both authorized and in-game'''.  That is, both OnClientAuthorized() '''and''' OnClientPutInServer() have been invoked.  Only used to override admin checks.  For everything else, use OnClientPostAdminCheck.
+
*[https://sm.alliedmods.net/new-api/clients/OnClientPostAdminCheck OnClientPostAdminCheck()] - Called after the player is '''both authorized and in-game'''.  This is the best callback for checking administrative access after connect.
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=823& OnClientPostAdminFilter()] - Called after the player is '''both authorized and in-game'''.  Used to override the permissions an admin has.  For everything else, use OnClientPostAdminCheck.
+
*[https://sm.alliedmods.net/new-api/clients/OnClientDisconnect OnClientDisconnect()] - Called when a player's disconnection starts.  '''This is paired to OnClientConnected().'''
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=396& OnClientPostAdminCheck()] - Called after the player is '''both authorized and in-game'''.  This is the best callback for checking administrative access after connect.
+
*[https://sm.alliedmods.net/new-api/clients/OnClientDisconnect_Post OnClientDisconnect_Post()] - Called when a player's disconnection ends.  '''This is paired to OnClientConnected().'''
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=390& OnClientDisconnect()] - Called when a player's disconnection starts.  '''This is paired to OnClientConnect().'''
 
*[http://docs.sourcemod.net/api/index.php?fastload=show&id=391& OnClientDisconnect_Post()] - Called when a player's disconnection ends.  '''This is paired to OnClientConnect().'''
 
  
 
=Frequently Asked Questions=
 
=Frequently Asked Questions=
Line 555: Line 436:
 
All clients are fully disconnected before the map changes.  They are all reconnected after the next map starts.
 
All clients are fully disconnected before the map changes.  They are all reconnected after the next map starts.
  
 +
If you only want to detect when a client initially connects or leaves your server, hook the [[Generic Source Server Events#player_connect|player_connect]] or [[Generic Source Server Events#player_disconnect|player_disconnect]] events respectively.
 +
 +
==Why am I getting "function prototypes do not match" errors?==
 +
When you see this error, you'll most likely find that the issue comes from any callback functions referenced in the line(s) that are causing the error.
 +
 +
When you call a function that takes another function as a callback, the callback function must be declared with the correct number of parameter and return types.
 +
 +
For example, [https://sm.alliedmods.net/new-api/console/RegConsoleCmd <tt>RegConsoleCommand</tt>] must be called with a callback that has with the exact arguments and return type as specified by the [https://sm.alliedmods.net/new-api/console/ConCmd <tt>ConCmd</tt>] definition.
  
 
=Further Reading=
 
=Further Reading=
For further reading, see the "Scripting" section at the [http://docs.sourcemod.net/ SourceMod Documentation].
+
For further reading, see the "Scripting" section at the [http://docs.sourcemod.net/ SourceMod Documentation], as well as [https://wiki.alliedmods.net/Scripting_FAQ_(SourceMod) Yak's FAQs on Scripting].
  
 
[[Category:SourceMod Scripting]]
 
[[Category:SourceMod Scripting]]
  
 
{{LanguageSwitch}}
 
{{LanguageSwitch}}

Revision as of 17:23, 9 August 2019

This guide will give you a basic introduction to writing a SourceMod plugin. If you are not familiar with the SourcePawn language, it is recommended that you at least briefly read the Introduction to SourcePawn article.

For information on compiling plugins, see Compiling SourceMod Plugins. You can use SPEdit, Crimson Editor, PSPad, UltraEdit, Notepad++, TextPad, Pawn Studio, BasicPawn or any other text editor you're comfortable with to write plugins.

Starting from scratch

Open your favorite text editor and create a new empty file. When you have an empty file you can just start writing code using the core language, however, you will not be able to use any of SourceMod features because the compiler does not know about them. This is done deliberately so it is possible to use SourcePawn outside of SourceMod. But since we are writing a SourceMod plugin, it is a good idea to enable access to SourceMod features first. This is done using #include directive. It tells the compiler to "paste" the code from another file into yours.

#include <sourcemod>

How does this work? First of all, note that we enclosed file name into angle brackets. Angle brackets tell the compiler to look in the default include directory. By default, it is scripting/include. You can open it right now and see a lot of inc files there. Those are SourceMod include files that describe various functions, tags and other features available for SourceMod plugins. The files are plain-text and you are encouraged to read them. You will notice, however, that there's not much code in there, certainly not enough to implement all the great features of SourceMod, so where are they? They are implemented inside a SourceMod core which is written in C++ and is compiled into binary files which end up in bin directory. So how does your SourcePawn code and SM core link together if the compiler doesn't know about the existence of the latter? SourceMod include files are written specially, so they say that the implementation of functions is somewhere else. The compiler understands that and generates a special code that says that this function call is going outside. When SourceMod loads your plugin, it inspects these bits of code and substitutes it's own internal functions instead. This is called dynamic linking.

Setting up plugin info

Now that we got access to SourceMod features, it is time to set up the information that will be displayed via sm plugins list command. No one likes unnamed plugins. To do that we are going to look inside sourcemod.inc file and see the format that information should be declared. It's always helpful to look inside SM include files to find out information you don't know. There is also an API documentation but it can be outdated and it only has SM core files so if your plugin is going to use any third party extension or another plugin, you will have to study inc files. So, open sourcemod.inc and scroll down a bit until you see this:

/**
 * Plugin public information.
 */
struct Plugin
{
   public const char[] name;		/**< Plugin Name */
   public const char[] description;	/**< Plugin Description */
   public const char[] author;		/**< Plugin Author */
   public const char[] version;		/**< Plugin Version */
   public const char[] url;			/**< Plugin URL */
};

and this:

/**
 * Declare this as a struct in your plugin to expose its information.
 * Example:
 *
 * public Plugin myinfo =
 * {
 *    name = "My Plugin",
 *    //etc
 * };
 */
public Plugin myinfo;

It tells us that we need to create a global public variable myinfo which must be of type Plugin which is a struct with 5 fields which themselves are strings. It may sound complicated for a beginner but it's easy. Let's go ahead and create one:

public Plugin myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0",
	url = "http://www.sourcemod.net/"
};

The public keyword means that SourceMod will be able to directly access our variable. Plugin: defines a type of our variable. myinfo is, obviously, a name of our variable as required by SourceMod. You see that we initialize it right away. This is the preferable way to fill out plugin info.

After that the full code of your plugin should look like this:

#include <sourcemod>
 
public Plugin myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0",
	url = "http://www.sourcemod.net/"
};

Getting code to run

We already include SourceMod features and filled up or plugin info. We now have a perfectly well-formed plugin which can be compiled and loaded by SourceMod. However, there is one problem - it does nothing. You might be tempted to just start writing a code after myinfo declaration just to see that it will not compile. SourcePawn, unlike other scripting languages like Lua, does not allow a code to be outside of functions. After reading that, you may probably want to just define some function, name it main probably, compile and load a plugin and see that your code never gets called. So how do we make SourceMod call our code? For this exact reason, we have forwards. Forwards are function prototypes declared by one party that can be implemented by another party as a callback. When a first party starts a forward call, all parties that have matching callbacks receive the call. SourceMod declares a plenty of interesting forwards that we can implement. As you can see, forwards are the only way to get our code executed, keep that in mind. So let's implement OnPluginStart forward. As you may have guessed, it is called when our plugin starts. To do that, we'll have to look up the declaration of OnPluginStart. It is declared inside sourcemod.inc, a file we are already familiar with, let's find it:

/**
 * Called when the plugin is fully initialized and all known external references 
 * are resolved. This is only called once in the lifetime of the plugin, and is 
 * paired with OnPluginEnd().
 *
 * If any run-time error is thrown during this callback, the plugin will be marked 
 * as failed.
 *
 * It is not necessary to close any handles or remove hooks in this function.  
 * SourceMod guarantees that plugin shutdown automatically and correctly releases 
 * all resources.
 *
 * @noreturn
 */
forward void OnPluginStart();

Empty parentheses tell us that no arguments are passed inside this forward, @noreturn inside documentation tells us that we don't have to return anything, pretty simple forward. So how to write a correct callback for it? Firstly, our callback must have the same name, so it's OnPluginStart, secondly, our callback should have the same number of arguments, none in this case, and lastly, SourceMod needs to be able to call our callback so it needs to be public. So the implementation looks like this:

public void OnPluginStart()
{
}

Now we can write code inside curly braces and it will be executed when our plugin starts. Let's output "Hello world!" to server console. To do that we are going to use PrintToServer function. It is declared inside console.inc, however, we don't need to manually include console.inc because it is included automatically as part of sourcemod.inc.

/**
 * Sends a message to the server console.
 *
 * @param format		Formatting rules.
 * @param ...			Variable number of format parameters.
 * @noreturn
 */
native int PrintToServer(const char[] format, any ...);

As you can see, this is a native function. It is implemented inside SM core. Judging by it's arguments, we can see that it is a format class function. However, we don't need any formatting right now, so let's just pass "Hello world!" string as an only argument:

public void OnPluginStart()
{
	PrintToServer("Hello world!");
}

That's it! The full code of your plugin should look like this:

#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 void OnPluginStart()
{
	PrintToServer("Hello world!");
}

Compile and load your plugin on your server and see for yourself that the message is displayed in the server console.

Includes

Pawn requires include files, much like C requires header files. Include files list all of the structures, functions, callbacks, and tags that are available. There are three types of include files:

  • Core - sourcemod.inc and anything it includes. These are all provided by SourceMod's Core.
  • Extension - adds a dependency against a certain extension.
  • Plugin - adds a dependency against a certain plugin.

Include files are loaded using the #include compiler directive.

Commands

Our first example will be writing a simple admin command to slap a player. We'll continue to extend this example with more features until we have a final, complete result.

Declaration

First, let's look at what an admin command requires. Admin commands are registered using the RegAdminCmd function. They require a name, a callback function, and default admin flags.

The callback function is what's invoked every time the command is used. Click here to see its prototype. Example:

public void OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
}
 
public Action Command_MySlap(int client, int args)
{
}

Now we've successfully implemented a command -- though it doesn't do anything yet. In fact, it will say "Unknown command" if you use it! This is because you're not returning Plugin_Handled in your callback. Since you haven't, SourceMod believes you didn't want the Source Engine to know the command was registered, and it handles it so. The reason SourceMod expects your function to return Plugin_Handled is because of the Action tag you put in your function's prototype. The Action tag specifies that Command_MySlap must return one of four things. See the Action enumeration in the sourcemod API to learn more about these return types and when to use them.

public Action Command_MySlap(int client, int args)
{
	return Plugin_Handled;
}

Now the command will report no error, but it still won't do anything. This is because returning "Plugin_Handled" in a command callback will prevent the engine from processing the command. The engine will never even see that the command was run. This is what you will want to do if you are registering a completely new command through SourceMod.

Implementation

Let's decide what the command will look like. Let's have it act like the default sm_slap command:

sm_myslap <name|#userid> [damage]

To implement this, we'll need a few steps:

  • Get the input from the console. For this we use GetCmdArg().
  • Find a matching player. For this we use FindTarget().
  • Slap them. For this we use SlapPlayer(), which requires including sdktools, an extension bundled with SourceMod.
  • Respond to the admin. For this we use ReplyToCommand().

Full example:

#include <sourcemod>
#include <sdktools>
 
public Plugin myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
}
 
public void OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
	LoadTranslations("common.phrases.txt"); // Required for FindTarget fail reply
}
 
public Action Command_MySlap(int client, int args)
{
	char arg1[32], arg2[32];
 
	/* By default, we set damage = 0 */
	int damage = 0;
 
	/* Get the first argument */
	GetCmdArg(1, arg1, sizeof(arg1));
 
	/* If there are 2 or more arguments, we set damage to
	 * what the user specified. If a damage isn't specified
	 * then it will stay zero. */
	if (args >= 2)
	{
		GetCmdArg(2, arg2, sizeof(arg2));
		damage = StringToInt(arg2);
	}
 
	/* Try and find a matching player */
	int target = FindTarget(client, arg1);
	if (target == -1)
	{
		/* FindTarget() automatically replies with the 
		 * failure reason and returns -1 so we know not 
		 * to continue
		 */
		return Plugin_Handled;
	}
 
	SlapPlayer(target, damage);
 
	char name[MAX_NAME_LENGTH];
 
	GetClientName(target, name, sizeof(name));
	ReplyToCommand(client, "[SM] You slapped %s for %d damage!", name, damage);
 
	return Plugin_Handled;
}

For more information on what %s and %d are, see Format Class Functions. Note that you never need to unregister or remove your admin command. When a plugin is unloaded, SourceMod cleans it up for you.

ConVars

ConVars, also known as cvars, are global console variables in the Source engine. They can have integer, float, or string values. ConVar accessing is done through Handles. Since ConVars are global, you do not need to close ConVar Handles (in fact, you cannot).

The handy feature of ConVars is that they are easy for users to configure. They can be placed in any .cfg file, such as server.cfg or sourcemod.cfg. To make this easier, SourceMod has an AutoExecConfig() function. This function will automatically build a default .cfg file containing all of your cvars, annotated with comments, for users. It is highly recommended that you call this if you have customizable ConVars.

Let's extend your example from earlier with a new ConVar. Our ConVar will be g_cvarMySlapDamage and will specify the default damage someone is slapped for if no damage is specified.

ConVar g_cvarMySlapDamage = null;
 
public void OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
 
	g_cvarMySlapDamage = CreateConVar("sm_myslap_damage", "5", "Default slap damage");
	AutoExecConfig(true, "plugin_myslap");
}
 
public Action Command_MySlap(int client, int args)
{
	char arg1[32], arg2[32];
	int damage = g_cvarMySlapDamage.IntValue;
 
	/* The rest remains unchanged! */

Showing Activity, Logging

Almost all admin commands should log their activity, and some admin commands should show their activity to in-game clients. This can be done via the LogAction() and ShowActivity2() functions. The exact functionality of ShowActivity2() is determined by the sm_show_activity cvar.

For example, let's rewrite the last few lines of our slap command:

	SlapPlayer(target, damage);
 
	char name[MAX_NAME_LENGTH];
 
	GetClientName(target, name, sizeof(name));
 
	ShowActivity2(client, "[SM] ", "Slapped %s for %d damage!", name, damage);
	LogAction(client, target, "\"%L\" slapped \"%L\" (damage %d)", client, target, damage);
 
	return Plugin_Handled;
}

Multiple Targets

To fully complete our slap demonstration, let's make it support multiple targets. SourceMod's targeting system is quite advanced, so using it may seem complicated at first.

The function we use is ProcessTargetString(). It takes in input from the console and returns a list of matching clients. It also returns a noun that will identify either a single client or describe a list of clients. The idea is that each client is then processed, but the activity shown to all players is only processed once. This reduces screen spam.

This method of target processing is used for almost every admin command in SourceMod, and in fact, FindTarget() is just a simplified version.

Full, final example:

#include <sourcemod>
#include <sdktools>
 
ConVar g_cvarMySlapDamage = null;
 
public Plugin myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
}
 
public void OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY);
	LoadTranslations("common.phrases.txt");
 
	g_cvarMySlapDamage = CreateConVar("sm_myslap_damage", "5", "Default slap damage");
	AutoExecConfig(true, "plugin_myslap");
}
 
public Action Command_MySlap(int client, int args)
{
	char arg1[32], arg2[32];
	int damage = g_cvarMySlapDamage.IntValue;
 
	/* Get the first argument */
	GetCmdArg(1, arg1, sizeof(arg1));
 
	/* If there are 2 or more arguments, and the second argument fetch 
	 * is successful, convert it to an integer.
	 */
	if (args >= 2 && GetCmdArg(2, arg2, sizeof(arg2)))
	{
		damage = StringToInt(arg2);
	}
 
	/**
	 * target_name - stores the noun identifying the target(s)
	 * target_list - array to store clients
	 * target_count - variable to store number of clients
	 * tn_is_ml - stores whether the noun must be translated
	 */
	char target_name[MAX_TARGET_LENGTH];
	int target_list[MAXPLAYERS], target_count;
	bool tn_is_ml;
 
	if ((target_count = ProcessTargetString(
			arg1,
			client,
			target_list,
			MAXPLAYERS,
			COMMAND_FILTER_ALIVE, /* Only allow alive players */
			target_name,
			sizeof(target_name),
			tn_is_ml)) <= 0)
	{
		/* This function replies to the admin with a failure message */
		ReplyToTargetError(client, target_count);
		return Plugin_Handled;
	}
 
	for (int i = 0; i < target_count; i++)
	{
		SlapPlayer(target_list[i], damage);
		LogAction(client, target_list[i], "\"%L\" slapped \"%L\" (damage %d)", client, target_list[i], damage);
	}
 
	if (tn_is_ml)
	{
		ShowActivity2(client, "[SM] ", "Slapped %t for %d damage!", target_name, damage);
	}
	else
	{
		ShowActivity2(client, "[SM] ", "Slapped %s for %d damage!", target_name, damage);
	}
 
	return Plugin_Handled;
}

Events

Events are informational notification messages passed between objects in the server. Many are also passed from the server to the client. They are defined in .res files under the hl2/resource folder and resource folders of specific mods. For a basic listing, see Source Game Events.

It is important to note a few concepts about events:

  • They are almost always informational. That is, blocking player_death will not stop a player from dying. It may block a HUD or console message or something else minor.
  • They almost always use userids instead of client indexes.
  • Just because it is in a resource file does not mean it is ever called, or works the way you expect it to. Mods are notorious for not properly documenting their event functionality.

An example of finding when a player dies:

public void OnPluginStart()
{
   HookEvent("player_death", Event_PlayerDeath);
}
 
public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
   int victim_id = event.GetInt("userid");
   int attacker_id = event.GetInt("attacker");
 
   int victim = GetClientOfUserId(victim_id);
   int attacker = GetClientOfUserId(attacker_id);
 
   /* CODE */
}

Callback Orders and Pairing

SourceMod has a number of builtin callbacks about the state of the server and plugin. Some of these are paired in special ways which can confuse users.

Pairing

Pairing is SourceMod terminology. Examples of it are:

  • OnMapEnd() cannot be called without an OnMapStart(), and if OnMapStart() is called, it cannot be called again without an OnMapEnd().
  • OnClientConnected(N) for a given client N will only be called once until an OnClientDisconnected(N) for the same client N is called (which is guaranteed to happen).

There is a formal definition of SourceMod's pairing. For two functions X and Y, both with input A, the following conditions hold:

  • If X is invoked with input A, it cannot be invoked again with the same input unless Y is called with input A.
  • If X is invoked with input A, it is guaranteed that Y will, at some point, be called with input A.
  • Y cannot be invoked with any input A unless X was called first with input A.
  • The relationship is described as, "X is paired with Y," and "Y is paired to X."

General Callbacks

These callbacks are listed in the order they are called, in the lifetime of a plugin and the server.

  • AskPluginLoad2() - Called once, immediately after the plugin is loaded from the disk. This function can be used to stop a plugin from loading and return a custom error message; return APLRes_Failure and use strcopy on to replace the error string. All CreateNative and RegPluginLibrary calls should be done here.
  • OnPluginStart() - Called once, after the plugin has been fully initialized and can proceed to load. Any run-time errors in this function will cause the plugin to fail to load. This is paired with OnPluginEnd().
  • OnAllPluginsLoaded() - Called once, after all non-late loaded plugins have called OnPluginStart.
  • OnMapStart() - Called every time the map loads. If the plugin is loaded late, and the map has already started, this function is called anyway after load, in order to preserve pairing. This function is paired with OnMapEnd().
  • OnConfigsExecuted() - Called once per map-change after servercfgfile (usually server.cfg), sourcemod.cfg, and all plugin config files have finished executing. If a plugin is loaded after this has happened, the callback is called anyway, in order to preserve pairing. This function is paired with OnMapEnd().
  • At this point, most game callbacks can occur, such as events and callbacks involving clients (or other things, like OnGameFrame).
  • OnMapEnd() - Called when the map is about to end. At this point, all clients are disconnected, but TIMER_NO_MAPCHANGE timers are not yet destroyed. This function is paired to OnMapStart().
  • OnPluginEnd() - Called once, immediately before the plugin is unloaded. This function is paired to OnPluginStart().

Client Callbacks

These callbacks are listed in no specific order, however, their documentation holds for both fake and real clients.

  • OnClientConnect() - Called when a player initiates a connection. You can block a player from connecting by returning Plugin_Stop and setting rejectmsg to an error message.
  • OnClientConnected() - Called after a player connects. Signifies that the player is in-game and IsClientConnected() will return true. This is paired with OnClientDisconnect() for successful connections only.
  • OnClientAuthorized() - Called when a player gets a Steam ID. It is important to note that this may never be called. It may occur any time in between OnClientConnected and OnClientPreAdminCheck/OnClientDisconnect. Do not rely on it unless you are writing something that needs Steam IDs, and even then you should use OnClientPostAdminCheck().
  • OnClientPutInServer() - Signifies that the player is in-game and IsClientInGame() will return true.
  • OnClientPostAdminCheck() - Called after the player is both authorized and in-game. This is the best callback for checking administrative access after connect.
  • OnClientDisconnect() - Called when a player's disconnection starts. This is paired to OnClientConnected().
  • OnClientDisconnect_Post() - Called when a player's disconnection ends. This is paired to OnClientConnected().

Frequently Asked Questions

Are plugins reloaded every mapchange?

Plugins, by default, are not reloaded on mapchange unless their timestamp changes. This is a feature so plugin authors have more flexibility with the state of their plugins.

Do I need to call CloseHandle in OnPluginEnd?

No. SourceMod automatically closes your Handles when your plugin is unloaded, in order to prevent memory errors.

Do I need to #include every individual .inc?

No. #include <sourcemod> will give you 95% of the .incs. Similarly, #include <sdktools> includes everything starting with <sdktools>.

Why don't some events fire?

There is no guarantee that events will fire. The event listing is not a specification, it is a list of the events that a game is capable of firing. Whether the game actually fires them is up to Valve or the developer.

Do I need to CloseHandle timers?

No. In fact, doing so may cause errors. Timers naturally die on their own unless they are infinite timers, in which case you can use KillTimer() or die gracefully by returning Plugin_Stop in the callback.

Are clients disconnected on mapchange?

All clients are fully disconnected before the map changes. They are all reconnected after the next map starts.

If you only want to detect when a client initially connects or leaves your server, hook the player_connect or player_disconnect events respectively.

Why am I getting "function prototypes do not match" errors?

When you see this error, you'll most likely find that the issue comes from any callback functions referenced in the line(s) that are causing the error.

When you call a function that takes another function as a callback, the callback function must be declared with the correct number of parameter and return types.

For example, RegConsoleCommand must be called with a callback that has with the exact arguments and return type as specified by the ConCmd definition.

Further Reading

For further reading, see the "Scripting" section at the SourceMod Documentation, as well as Yak's FAQs on Scripting.

Warning: This template (and by extension, language format) should not be used, any pages using it should be switched to Template:Languages

View this page in:  English  Russian  简体中文(Simplified Chinese)