Difference between revisions of "User:Nosoop/Guide/Basics"

From AlliedModders Wiki
Jump to: navigation, search
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.

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.

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.

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.

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

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.

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

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

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 anymore
  • EntIndexToEntRef / EntRefToEntIndex returns INVALID_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 with IsValidEntity.
  • 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;
}

Note:This section is a work-in-progress. Work on a 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.

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

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.