Difference between revisions of "User:Nosoop/Guide/Basics"
m |
(→Handles: Add example DataPack code) |
||
Line 194: | Line 194: | ||
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. | ||
+ | |||
+ | When you called <code>CreateTimer</code> 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 in that 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 <code>DataPack</code> to pass more values around. Take a look at the following code: | ||
+ | |||
+ | <pre>#include <sourcemod> | ||
+ | |||
+ | public void OnPluginStart() { | ||
+ | RegClientCommand("sm_delayedecho", DelayedEcho); | ||
+ | } | ||
+ | |||
+ | public void DelayedEcho(int client, int argc) { | ||
+ | char message[64]; | ||
+ | GetCmdArg(2, message, sizeof(message)); | ||
+ | |||
+ | DataPack pack; | ||
+ | CreateDataTimer(5.0, EchoResponse, pack); | ||
+ | |||
+ | pack.WriteCell(GetClientSerial(client)); | ||
+ | pack.WriteString(message); | ||
+ | } | ||
+ | |||
+ | public 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. Work on a <code>DataPack</code> example as a follow-up to the timer example above.}} | {{Note|This section is a work-in-progress. Work on a <code>DataPack</code> example as a follow-up to the timer example above.}} |
Revision as of 18:32, 11 October 2020
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> 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 01_hello.sp
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.
Immediately after the line is a blank line. It serves no other purpose other than to visually organize code, but organization is good.
public void OnPluginStart() {
This line defines a function named OnPluginStart
in SourceMod. It is a 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
function (because of the public visibility mentioned previously), then ran the code within the curly brackets.
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); } public 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.
public 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.
any
, 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 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.
Every entity has both netprops and datamaps.
- sendprops / netprops are properties that are sent over the network.
- dataprops / datamaps are properties that are saved / restored. (In all honesty, I'm not sure what this means. 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.
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
.
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).
One common error many developers make is to directly pass 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, 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_REF
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
.GetClientUserId
/GetClientFromUserId
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 in that 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() { RegClientCommand("sm_delayedecho", DelayedEcho); } public void DelayedEcho(int client, int argc) { char message[64]; GetCmdArg(2, message, sizeof(message)); DataPack pack; CreateDataTimer(5.0, EchoResponse, pack); pack.WriteCell(GetClientSerial(client)); pack.WriteString(message); } public 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; }
DataPack
example as a follow-up to the timer example above.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.
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.