Difference between revisions of "User:Nosoop/Guide/Basics"
(Add debugging tips) |
(Insert notes on comments) |
||
(26 intermediate revisions by the same user not shown) | |||
Line 8: | Line 8: | ||
<pre class="sourcepawn">#include <sourcemod> | <pre class="sourcepawn">#include <sourcemod> | ||
+ | |||
+ | // this is a comment | ||
public void OnPluginStart() { | public void OnPluginStart() { | ||
Line 46: | Line 48: | ||
You'll need to be able to read these sorts of error messages and understand the common ones, because no developer is immune to writing code without any errors all the time. | You'll need to be able to read these sorts of error messages and understand the common ones, because no developer is immune to writing code without any errors all the time. | ||
− | In this case, the compiler tells us that <code>01_hello.sp</code> has an error around lines 4 and 5, and gives us information about what caused it to not compile the code. | + | In this case, the compiler tells us that my <code>01_hello.sp</code> (intentionally made to fail - the code above should compile correctly) has an error around lines 4 and 5, and gives us information about what caused it to not compile the code. |
{{Note|Warnings are not errors. | {{Note|Warnings are not errors. | ||
Line 72: | Line 74: | ||
<pre class="sourcepawn">#include <sourcemod> | <pre class="sourcepawn">#include <sourcemod> | ||
</pre> | </pre> | ||
− | This line is a ''preprocessor directive''. Lines starting with <code>#</code> are instructions for the compiler (rather, the preprocessor that examines the code before compilation occurs). In this case, <code>#include <sourcemod></code> tells the preprocessor to include the files that make up SourceMod's standard library. | + | This line is a ''preprocessor directive''. Lines starting with <code>#</code> are instructions for the compiler (rather, the preprocessor that examines the code before compilation occurs). In this case, <code>#include <sourcemod></code> tells the preprocessor to include the files that make up SourceMod's standard library. There are other first- and third-party libraries that you may want to include later on. |
Immediately after the line is a blank line. It serves no other purpose other than to visually organize code, but organization is good. | Immediately after the line is a blank line. It serves no other purpose other than to visually organize code, but organization is good. | ||
+ | |||
+ | <pre>// this is a comment</pre> | ||
+ | |||
+ | Two forward slashes (<code>//</code>) indicate the start of a start of a single-line comment. Single-line comments start from those forward slashes and continue up until the end of the line of text. Comments are not compiled into the actual code, but serve as informational pieces of text for the reader. These will be used later in the guide to streamline reading. | ||
<pre>public void OnPluginStart() { | <pre>public void OnPluginStart() { | ||
</pre> | </pre> | ||
− | This line defines a function named | + | This line defines a function named {{SourceMod API|file=sourcemod|function=OnPluginStart}} in SourceMod (click the link to see the documentation for it). It is a ''forward'' function that has <code>public</code> visibility, returns nothing (indicated by <code>void</code>), and has no parameters (indicated by the lack of text between the parentheses <code>()</code>). The curly brackets, <code>{}</code>, indicate the function body. |
− | SourceMod plugins are callback-based. In this case, when the plugin was loaded, SourceMod found that it had an <code>OnPluginStart</code> function (because of the public visibility mentioned previously), then ran the code within the curly brackets. | + | SourceMod plugins are callback-based. In this case, when the plugin was loaded, SourceMod found that it had an <code>OnPluginStart</code> ''forward'' function (because of the public visibility mentioned previously), then ran the code within the curly brackets. If <code>public</code> is not specified, SourceMod won't know that there's a function it should run automatically. |
<pre> PrintToServer("Hello, world!"); | <pre> PrintToServer("Hello, world!"); | ||
Line 100: | Line 106: | ||
} | } | ||
− | + | Action SayHello(int client, int argc) { | |
ReplyToCommand(client, "Hello, %N!", client); | ReplyToCommand(client, "Hello, %N!", client); | ||
return Plugin_Handled; | return Plugin_Handled; | ||
Line 125: | Line 131: | ||
</pre> | </pre> | ||
− | When the plugin is loaded, | + | When the plugin is loaded, {{SourceMod API|file=console|function=RegConsoleCmd}} is called. That function registers a console command <code>sm_hello</code> — when that command is invoked, it invokes the <code>SayHello</code> callback function. The function is internal to the plugin, so it does not need to be marked as <code>public</code> like <code>OnPluginStart</code> is. |
− | <pre> | + | <pre>Action SayHello(int client, int argc) { |
</pre> | </pre> | ||
− | <code>SayHello</code> is a user-defined function that follows the ''prototype'' defined by | + | <code>SayHello</code> is a user-defined function that follows the ''prototype'' defined by {{SourceMod API|file=console|function=ConCmd}} . A prototype defines the return type and the parameter types of the function. It must follow that prototype exactly; failure to do so will cause the error "function prototypes do not match", which, to most newer developers, is a familiar compilation error message with words that don't have any sense to them. |
+ | |||
+ | In this case, the <code>SayHello</code> function receives a value named <code>client</code> of type <code>int</code>, and one named <code>argc</code>, also of type <code>int</code>; it returns a value of type <code>Action</code>. Refer to the link to <code>ConCmd</code> above for what those parameters mean. | ||
− | {{Note|If the prototype specifies <code>any</code>, the function can substitute that parameter type with any different non-array parameter. If the prototype specifies <code>Handle</code>, the function can substitute the parameter with a <code>Handle</code>-derived type.}} | + | {{Note|If the prototype specifies <code>any</code> as one of the parameter types, the function can substitute that parameter type with any different non-array parameter. If the prototype specifies <code>Handle</code>, the function can substitute the parameter with a <code>Handle</code>-derived type.}} |
Next line: | Next line: | ||
Line 139: | Line 147: | ||
<code>ReplyToCommand</code> is a function that tells us to respond to a client (by console or text chat, depending on how the client invoked the command). It takes a minimum of two arguments, the client to respond to, a format string, and optionally a list of format parameters. | <code>ReplyToCommand</code> is a function that tells us to respond to a client (by console or text chat, depending on how the client invoked the command). It takes a minimum of two arguments, the client to respond to, a format string, and optionally a list of format parameters. | ||
− | In format strings, the <code>%</code> character is special — it indicates a [[Format_Class_Functions_(SourceMod_Scripting)#Format_Specifiers|''format specifier'']]. The link has more information on what specifiers do, but for now just know that <code>%N</code> means that it takes the one of the later format parameters and outputs the player's name. | + | In format strings, the <code>%</code> character is special — it indicates a [[Format_Class_Functions_(SourceMod_Scripting)#Format_Specifiers|''format specifier'']]. The link has more information on what specifiers do, but for now just know that <code>%N</code> means that it takes the one of the later format parameters (in this case, the second <code>client</code> parameter that was passed in to the <code>SayHello</code> function) and outputs the player's name. |
<pre> return Plugin_Handled; | <pre> return Plugin_Handled; | ||
</pre> | </pre> | ||
− | <code>return</code> is a keyword that indicates a value that should be given back to the calling function. The calling function is internal to SourceMod in this case. For a command callback, this is an | + | <code>return</code> is a keyword that indicates a value that should be given back to the calling function. The calling function is internal to SourceMod in this case. For a command callback, this is an {{SourceMod API|file=core|function=Action}} enumeration, and we use <code>Plugin_Handled</code> to tell the server that we acknowledge the command. It'll still work without it, but the server console will report "Unknown command" if you don't return with that value. |
= Marking Your Territory = | = Marking Your Territory = | ||
Line 157: | Line 165: | ||
In this section we will talk about entity send / data properties, and some examples on how to use them. | In this section we will talk about entity send / data properties, and some examples on how to use them. | ||
− | + | An ''entity'' is an instance of an object in the world. These include things like players, weapons, grenades, but also abstract things like spawn points, objective areas, and round timers. | |
− | + | Most entities of relevance have ''netprops'' and / or ''datamaps''. These specific names are Source Engine-specific constructs. | |
− | |||
− | While they are stored in different ways in the engine, they both correspond to (some, not all) member variables on the entity class. | + | * sendprops / netprops are properties that are sent over the network and intended to ensure the server and connected clients have the correct information. |
+ | * dataprops / datamaps are properties that are saved / restored. (In all honesty, I'm not sure what this means; probably related to either snapshots or game saves. Someone should edit this with a better explanation, because this is a wiki.) | ||
+ | |||
+ | While they are stored in different ways in the engine, they both correspond to (some, not all) member variables on the entity class that need to be kept track of in some way. | ||
Before we can actually work with them, we need to know which ones exist, so we'll dump them for your game. Open up your server console and type in the following: | Before we can actually work with them, we need to know which ones exist, so we'll dump them for your game. Open up your server console and type in the following: | ||
Line 171: | Line 181: | ||
</pre> | </pre> | ||
− | Check the server's game (mod) folder and you should see the <code>netprops.txt</code> and <code>datamaps.txt</code>. | + | Check the server's game (mod) folder and you should see the <code>netprops.txt</code> and <code>datamaps.txt</code> files. This will provide you with a list of properties associated with each entity class. |
+ | |||
+ | {{Note|Some names are listed as both netprops and dataprops on the same class. In that situation, using the netprop is preferred, as it will inform the engine that the change needs to be propagated to clients.}} | ||
+ | |||
+ | Here's an example of how to manipulate entities - this plugin zeros out the clip on the active weapon of the player that runs it: | ||
+ | |||
+ | <pre>#include <sourcemod> | ||
+ | |||
+ | public void OnPluginStart() { | ||
+ | RegConsoleCmd("sm_no_clip", RemoveClip); | ||
+ | } | ||
+ | |||
+ | Action RemoveClip(int client, int argc) { | ||
+ | if (client == 0) { | ||
+ | // this command is being run by the server console, not an actual player; we can't work with that | ||
+ | return Plugin_Handled; | ||
+ | } | ||
+ | |||
+ | // client is an entity index - we perform a lookup to get the entity assigned to m_hActiveWeapon, if one exists | ||
+ | int weapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); | ||
+ | if (IsValidEntity(weapon)) { | ||
+ | // the client has an active weapon - zero out its clip | ||
+ | SetEntProp(weapon, Prop_Send, "m_iClip1", 0); | ||
+ | } | ||
+ | return Plugin_Handled; | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | There are many ways to get entities depending on what you are doing. Some of them are passed directly as a callback argument, others you need to call a function to get one, and others you enumerate over a known allocated space to check. | ||
+ | |||
+ | {{Note|Some properties may not work when set directly.}} | ||
= Timers = | = Timers = | ||
Line 179: | Line 219: | ||
{{Note|This section is a work-in-progress.}} | {{Note|This section is a work-in-progress.}} | ||
− | One | + | <pre>#include <sourcemod> |
+ | |||
+ | public void OnPluginStart() { | ||
+ | RegConsoleCmd("sm_delayedhello", DelayedHello); | ||
+ | } | ||
+ | |||
+ | Action DelayedHello(int client, int argc) { | ||
+ | CreateTimer(5.0, DelayedHelloResponse, GetClientSerial(client)); | ||
+ | return Plugin_Handled; | ||
+ | } | ||
+ | |||
+ | Action DelayedHelloResponse(Handle timer, int clientserial) { | ||
+ | int client = GetClientFromSerial(clientserial); | ||
+ | if (client) { | ||
+ | PrintToChat(client, "... hello, %N.", client); | ||
+ | } | ||
+ | return Plugin_Handled; | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | One coding mistake that leads to confusion down the line is directly passing indices of clients or entities through a callback function. '''Don't do this!''' Such indices aren't unique, and there's no guarantee that you'll act on the same entity once the timer is done — the index may be occupied by a different entity or client (or none at all!), and it may have unexpected consequences. | ||
Don't pass client or entity indices. | Don't pass client or entity indices. | ||
Line 187: | Line 247: | ||
What you want to do in these cases is convert the index to a reference or serial value before sending it through the timer, then possibly unwrapping it on the other side. The following function pairs handle common cases: | What you want to do in these cases is convert the index to a reference or serial value before sending it through the timer, then possibly unwrapping it on the other side. The following function pairs handle common cases: | ||
− | * | + | * {{SourceMod API|file=clients|function=GetClientSerial}} / {{SourceMod API|file=clients|function=GetClientFromSerial}} returns 0 if the client isn't valid anymore |
− | * | + | * {{SourceMod API|file=halflife|function=EntIndexToEntRef}} / {{SourceMod API|file=halflife|function=EntRefToEntIndex}} returns <code>INVALID_ENT_REFERENCE</code> if the entity isn't valid; most, if not all SourceMod core functions that accept entity indices also accept entity references, so you may not need to unwrap the reference. Just check with {{SourceMod API|file=entity|function=IsValidEntity}}. |
− | * | + | ** If you're testing for entity equality e.g. with the result of {{SourceMod API|file=entity|function=GetEntPropEnt}}, ensure that the reference was converted back to an index, or that both values are references. |
+ | * {{SourceMod API|file=clients|function=GetClientUserId}} / {{SourceMod API|file=clients|function=GetClientOfUserId}} also returns 0 if the client isn't valid anymore. The values start at 1 and are incremented on player connections and map changes. | ||
= Handles = | = Handles = | ||
Line 195: | Line 256: | ||
A handle is a special type that represents a non-primitive type in SourceMod (any value that isn't a <code>float</code>, <code>int</code>, <code>bool</code>, or <code>char</code>). Such non-primitive types are implemented as C++ objects, managed by SourceMod, and exposed to SourcePawn plugins indirectly as an integer value. | A handle is a special type that represents a non-primitive type in SourceMod (any value that isn't a <code>float</code>, <code>int</code>, <code>bool</code>, or <code>char</code>). Such non-primitive types are implemented as C++ objects, managed by SourceMod, and exposed to SourcePawn plugins indirectly as an integer value. | ||
− | {{Note|This section is a work-in-progress.}} | + | When you called {{SourceMod API|file=timers|function=CreateTimer}} in the previous section, the function actually returned a timer handle value! We ignored it since we didn't need to worry about storing it (timer handles are a peculiar case), but you generally will want to store handle values in variables. |
+ | |||
+ | We'll expand on the timer example above by storing a few more values. You normally can only pass one value to the timer callback, so we will use a {{SourceMod API|file=datapack|function=DataPack}} to pass more values around. Take a look at the following code: | ||
+ | |||
+ | <pre>#include <sourcemod> | ||
+ | |||
+ | public void OnPluginStart() { | ||
+ | RegConsoleCmd("sm_delayedecho", DelayedEcho); | ||
+ | } | ||
+ | |||
+ | Action DelayedEcho(int client, int argc) { | ||
+ | char message[64]; | ||
+ | GetCmdArgString(message, sizeof(message)); | ||
+ | |||
+ | // CreateDataTimer instantiates a DataPack instance; normally you would create a handle with "new DataPack()" | ||
+ | DataPack pack; | ||
+ | CreateDataTimer(5.0, EchoResponse, pack); | ||
+ | |||
+ | pack.WriteCell(GetClientSerial(client)); | ||
+ | pack.WriteString(message); | ||
+ | return Plugin_Handled; | ||
+ | } | ||
+ | |||
+ | Action EchoResponse(Handle timer, DataPack pack) { | ||
+ | pack.Reset(); | ||
+ | int client = GetClientFromSerial(pack.ReadCell()); | ||
+ | char message[64]; | ||
+ | pack.ReadString(message, sizeof(message)); | ||
+ | |||
+ | if (client) { | ||
+ | PrintToChat(client, "Echo! %s", message); | ||
+ | } | ||
+ | return Plugin_Handled; | ||
+ | } | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | {{Note|This section is a work-in-progress. Discuss handle deletion, and why neither the Timer nor DataPack in the example above need to be deleted.}} | ||
+ | |||
+ | For other examples, see the [[Handles (SourceMod Scripting)|Handles page]] on the AlliedModders wiki. | ||
= SDKHooks = | = SDKHooks = | ||
Line 204: | Line 304: | ||
{{Note|This section is a work-in-progress.}} | {{Note|This section is a work-in-progress.}} | ||
+ | |||
+ | If SDKHooks doesn't have a hook on a function you're interested in, you can [[User:Nosoop/Guide/Advanced#Hooking_Game_Functions_.28with_DHooks.29|hook a specific function of your choosing with DHooks]]. | ||
= Debugging = | = Debugging = | ||
Line 209: | Line 311: | ||
As SourceMod is a third-party framework that is bolted on to a game engine, there's very little debug tooling to test plugins at runtime. | As SourceMod is a third-party framework that is bolted on to a game engine, there's very little debug tooling to test plugins at runtime. | ||
− | Most, if not all plugin authors use | + | Most, if not all plugin authors use {{SourceMod API|file=console|function=PrintToServer}} to print debug their code of runtime errors. Some tips: |
* Print the ''exact'' values that are being shown if they're relevant. It narrows down possible issues when you can see that something "is X", instead of just "is not Y". | * Print the ''exact'' values that are being shown if they're relevant. It narrows down possible issues when you can see that something "is X", instead of just "is not Y". | ||
− | * Sprinkle in messages at the start of <code>if</code>- or <code>else | + | * Sprinkle in messages at the start of <code>if</code>- or <code>else</code>- statements so you know what code paths are being taken in your plugin. |
= Further Reading = | = Further Reading = |
Latest revision as of 04:30, 16 April 2023
At this point you should have a server to test plugins on and a full development environment to write plugins with.
Contents
Hello!
As is tradition with other programming languages, we'll start with something that outputs Hello, world!
in some form. In this case, you'll install the plugin on your server and load it; the message will be printed in the server console.
Open up your editor, and enter the following code:
#include <sourcemod> // this is a comment public void OnPluginStart() { PrintToServer("Hello, world!"); }
Save this file as 01_hello.sp
. SourcePawn scripts always contain the .sp
extension.
Compile your code. Refer to your IDE-specific documentation for this.
On a successful compile, you should see the following:
SourcePawn Compiler 1.9 Copyright (c) 1997-2006 ITB CompuPhase Copyright (c) 2004-2017 AlliedModders LLC Code size: 2960 bytes Data size: 2240 bytes Stack/heap size: 16384 bytes Total requirements: 21584 bytes
The first few lines may be slightly different between SourceMod compiler versions, but as long as you see the four "bytes" lines, you've successfully compiled the plugin, and you should have a 01_hello.smx
file.
If there was an error in compilation, you'll see something like this instead:
SourcePawn Compiler 1.9 Copyright (c) 1997-2006 ITB CompuPhase Copyright (c) 2004-2017 AlliedModders LLC 01_hello.sp(4 -- 5) : error 001: expected token: ",", but found "}" 1 Error.
You'll need to be able to read these sorts of error messages and understand the common ones, because no developer is immune to writing code without any errors all the time.
In this case, the compiler tells us that my 01_hello.sp
(intentionally made to fail - the code above should compile correctly) has an error around lines 4 and 5, and gives us information about what caused it to not compile the code.
Running the Plugin
Copy the 01_hello.smx
file to your game server's addons/sourcemod/plugins/
directory.
If the server is already running, get to the server console and type sm plugins load 01_hello
to load the plugin. You should see the following:
Hello, world! [SM] Loaded plugin 01_hello.smx successfully.
If so, congratulations! You just wrote your first plugin.
What's in a Plugin
Adding text to a file, compiling it, and running the resulting plugin is one thing; it's much more important to understand what the text you're writing actually does.
We'll start from the top:
#include <sourcemod>
This line is a preprocessor directive. Lines starting with #
are instructions for the compiler (rather, the preprocessor that examines the code before compilation occurs). In this case, #include <sourcemod>
tells the preprocessor to include the files that make up SourceMod's standard library. There are other first- and third-party libraries that you may want to include later on.
Immediately after the line is a blank line. It serves no other purpose other than to visually organize code, but organization is good.
// this is a comment
Two forward slashes (//
) indicate the start of a start of a single-line comment. Single-line comments start from those forward slashes and continue up until the end of the line of text. Comments are not compiled into the actual code, but serve as informational pieces of text for the reader. These will be used later in the guide to streamline reading.
public void OnPluginStart() {
This line defines a function named OnPluginStart
in SourceMod (click the link to see the documentation for it). It is a forward function that has public
visibility, returns nothing (indicated by void
), and has no parameters (indicated by the lack of text between the parentheses ()
). The curly brackets, {}
, indicate the function body.
SourceMod plugins are callback-based. In this case, when the plugin was loaded, SourceMod found that it had an OnPluginStart
forward function (because of the public visibility mentioned previously), then ran the code within the curly brackets. If public
is not specified, SourceMod won't know that there's a function it should run automatically.
PrintToServer("Hello, world!");
This line tells SourceMod to tell the game to print text to the server console. PrintToServer
is a function defined in SourceMod's standard library. The "Hello, world!"
string is passed as an argument to PrintToServer
.
The semicolon (;
) indicates the end of the statement.
The }
on the following line closes the function body and indicates the end of the OnPluginStart
function.
Listening to Commands
Continuing with saying hello to things, we'll now get the server to display a message in response to a player command. In doing so, we'll implement a callback function ourselves.
#include <sourcemod> public void OnPluginStart() { RegConsoleCmd("sm_hello", SayHello); } Action SayHello(int client, int argc) { ReplyToCommand(client, "Hello, %N!", client); return Plugin_Handled; }
Save the above code as 02_helloclient.sp
, compile, copy, and load. This time you'll only see the following:
[SM] Loaded plugin 02_helloclient.smx successfully.
Now, connect to your server and type sm_hello
in the client's developer console. You should get:
] sm_hello Hello, (your Steam name)!
You can also type /hello
or !hello
in text chat, and get the response back there. SourceMod automatically registers text chat commands with the sm_
prefix stripped.
We'll go a little faster here:
RegConsoleCmd("sm_hello", SayHello);
When the plugin is loaded, RegConsoleCmd
is called. That function registers a console command sm_hello
— when that command is invoked, it invokes the SayHello
callback function. The function is internal to the plugin, so it does not need to be marked as public
like OnPluginStart
is.
Action SayHello(int client, int argc) {
SayHello
is a user-defined function that follows the prototype defined by ConCmd
. A prototype defines the return type and the parameter types of the function. It must follow that prototype exactly; failure to do so will cause the error "function prototypes do not match", which, to most newer developers, is a familiar compilation error message with words that don't have any sense to them.
In this case, the SayHello
function receives a value named client
of type int
, and one named argc
, also of type int
; it returns a value of type Action
. Refer to the link to ConCmd
above for what those parameters mean.
any
as one of the parameter types, the function can substitute that parameter type with any different non-array parameter. If the prototype specifies Handle
, the function can substitute the parameter with a Handle
-derived type.Next line:
ReplyToCommand(client, "Hello, %N!", client);
ReplyToCommand
is a function that tells us to respond to a client (by console or text chat, depending on how the client invoked the command). It takes a minimum of two arguments, the client to respond to, a format string, and optionally a list of format parameters.
In format strings, the %
character is special — it indicates a format specifier. The link has more information on what specifiers do, but for now just know that %N
means that it takes the one of the later format parameters (in this case, the second client
parameter that was passed in to the SayHello
function) and outputs the player's name.
return Plugin_Handled;
return
is a keyword that indicates a value that should be given back to the calling function. The calling function is internal to SourceMod in this case. For a command callback, this is an Action
enumeration, and we use Plugin_Handled
to tell the server that we acknowledge the command. It'll still work without it, but the server console will report "Unknown command" if you don't return with that value.
Marking Your Territory
In this section we will discuss the Plugin
struct, which lets you provide plugin information when displayed in the plugin list.
Entity Properties
Entity properties are one of the core aspects of game modding.
In this section we will talk about entity send / data properties, and some examples on how to use them.
An entity is an instance of an object in the world. These include things like players, weapons, grenades, but also abstract things like spawn points, objective areas, and round timers.
Most entities of relevance have netprops and / or datamaps. These specific names are Source Engine-specific constructs.
- sendprops / netprops are properties that are sent over the network and intended to ensure the server and connected clients have the correct information.
- dataprops / datamaps are properties that are saved / restored. (In all honesty, I'm not sure what this means; probably related to either snapshots or game saves. Someone should edit this with a better explanation, because this is a wiki.)
While they are stored in different ways in the engine, they both correspond to (some, not all) member variables on the entity class that need to be kept track of in some way.
Before we can actually work with them, we need to know which ones exist, so we'll dump them for your game. Open up your server console and type in the following:
sm_dump_netprops netprops.txt sm_dump_datamaps datamaps.txt
Check the server's game (mod) folder and you should see the netprops.txt
and datamaps.txt
files. This will provide you with a list of properties associated with each entity class.
Here's an example of how to manipulate entities - this plugin zeros out the clip on the active weapon of the player that runs it:
#include <sourcemod> public void OnPluginStart() { RegConsoleCmd("sm_no_clip", RemoveClip); } Action RemoveClip(int client, int argc) { if (client == 0) { // this command is being run by the server console, not an actual player; we can't work with that return Plugin_Handled; } // client is an entity index - we perform a lookup to get the entity assigned to m_hActiveWeapon, if one exists int weapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); if (IsValidEntity(weapon)) { // the client has an active weapon - zero out its clip SetEntProp(weapon, Prop_Send, "m_iClip1", 0); } return Plugin_Handled; }
There are many ways to get entities depending on what you are doing. Some of them are passed directly as a callback argument, others you need to call a function to get one, and others you enumerate over a known allocated space to check.
Timers
In this section we'll talk about timers and how the any data
parameter works. We should also talk about passing ephemeral data asynchronously (entities and clients).
#include <sourcemod> public void OnPluginStart() { RegConsoleCmd("sm_delayedhello", DelayedHello); } Action DelayedHello(int client, int argc) { CreateTimer(5.0, DelayedHelloResponse, GetClientSerial(client)); return Plugin_Handled; } Action DelayedHelloResponse(Handle timer, int clientserial) { int client = GetClientFromSerial(clientserial); if (client) { PrintToChat(client, "... hello, %N.", client); } return Plugin_Handled; }
One coding mistake that leads to confusion down the line is directly passing indices of clients or entities through a callback function. Don't do this! Such indices aren't unique, and there's no guarantee that you'll act on the same entity once the timer is done — the index may be occupied by a different entity or client (or none at all!), and it may have unexpected consequences.
Don't pass client or entity indices.
Don't pass client or entity indices.
What you want to do in these cases is convert the index to a reference or serial value before sending it through the timer, then possibly unwrapping it on the other side. The following function pairs handle common cases:
GetClientSerial
/GetClientFromSerial
returns 0 if the client isn't valid anymoreEntIndexToEntRef
/EntRefToEntIndex
returnsINVALID_ENT_REFERENCE
if the entity isn't valid; most, if not all SourceMod core functions that accept entity indices also accept entity references, so you may not need to unwrap the reference. Just check withIsValidEntity
.- If you're testing for entity equality e.g. with the result of
GetEntPropEnt
, ensure that the reference was converted back to an index, or that both values are references.
- If you're testing for entity equality e.g. with the result of
GetClientUserId
/GetClientOfUserId
also returns 0 if the client isn't valid anymore. The values start at 1 and are incremented on player connections and map changes.
Handles
A handle is a special type that represents a non-primitive type in SourceMod (any value that isn't a float
, int
, bool
, or char
). Such non-primitive types are implemented as C++ objects, managed by SourceMod, and exposed to SourcePawn plugins indirectly as an integer value.
When you called CreateTimer
in the previous section, the function actually returned a timer handle value! We ignored it since we didn't need to worry about storing it (timer handles are a peculiar case), but you generally will want to store handle values in variables.
We'll expand on the timer example above by storing a few more values. You normally can only pass one value to the timer callback, so we will use a DataPack
to pass more values around. Take a look at the following code:
#include <sourcemod> public void OnPluginStart() { RegConsoleCmd("sm_delayedecho", DelayedEcho); } Action DelayedEcho(int client, int argc) { char message[64]; GetCmdArgString(message, sizeof(message)); // CreateDataTimer instantiates a DataPack instance; normally you would create a handle with "new DataPack()" DataPack pack; CreateDataTimer(5.0, EchoResponse, pack); pack.WriteCell(GetClientSerial(client)); pack.WriteString(message); return Plugin_Handled; } Action EchoResponse(Handle timer, DataPack pack) { pack.Reset(); int client = GetClientFromSerial(pack.ReadCell()); char message[64]; pack.ReadString(message, sizeof(message)); if (client) { PrintToChat(client, "Echo! %s", message); } return Plugin_Handled; }
For other examples, see the Handles page on the AlliedModders wiki.
SDKHooks
SDKHooks is a first-party SourceMod extension that provides a number of useful entity function hooks.
We'll cover SDKHooks here. Maybe. This should probably be shifted over to the quickref.
If SDKHooks doesn't have a hook on a function you're interested in, you can hook a specific function of your choosing with DHooks.
Debugging
As SourceMod is a third-party framework that is bolted on to a game engine, there's very little debug tooling to test plugins at runtime.
Most, if not all plugin authors use PrintToServer
to print debug their code of runtime errors. Some tips:
- Print the exact values that are being shown if they're relevant. It narrows down possible issues when you can see that something "is X", instead of just "is not Y".
- Sprinkle in messages at the start of
if
- orelse
- statements so you know what code paths are being taken in your plugin.
Further Reading
I'm hoping to have further lessons on writing code here, but for now, you can take a look at the Introduction to SourcePawn and Introduction to SourceMod Plugins wiki entries.
Information on SourceMod's standard library is available in the Scripting API Reference.