User:Nosoop/Guide/Basics

From AlliedModders Wiki
Jump to: navigation, search

At this point you should have a server to test plugins on and a full development environment to write plugins with.

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.

Note:Warnings are not errors. Learn to differentiate between the two. If you get a lot of messages when compiling a large plugin, prioritize resolving the errors. It's possible that some syntax error is causing all sorts of warnings down the line.

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.

Note:If the prototype specifies 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.

Note:This section is a work-in-progress.

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.

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:

#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.

Note:Some properties may not work when set directly.

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).

Note:This section is a work-in-progress.
#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 anymore
  • EntIndexToEntRef / EntRefToEntIndex returns INVALID_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 with IsValidEntity.
    • 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.
  • 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;
}

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 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.

Note:This section is a work-in-progress.

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- or else- 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.