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

From AlliedModders Wiki
Jump to: navigation, search
(Clarify formatting logic)
(Entity Properties: Note that net/data props are SE-only)
(8 intermediate revisions by the same user not shown)
Line 78: Line 78:
 
<pre>public void OnPluginStart() {
 
<pre>public void OnPluginStart() {
 
</pre>
 
</pre>
This line defines a function named <code>OnPluginStart</code> in SourceMod. It is a 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.
+
This line defines a function named {{SourceMod API|file=sourcemod|function=OnPluginStart}} in SourceMod. It is a 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> function (because of the public visibility mentioned previously), then ran the code within the curly brackets.
Line 100: Line 100:
 
}
 
}
  
public Action SayHello(int client, int argc) {
+
Action SayHello(int client, int argc) {
 
     ReplyToCommand(client, &quot;Hello, %N!&quot;, client);
 
     ReplyToCommand(client, &quot;Hello, %N!&quot;, client);
 
     return Plugin_Handled;
 
     return Plugin_Handled;
Line 125: Line 125:
 
</pre>
 
</pre>
  
When the plugin is loaded, [https://sm.alliedmods.net/new-api/console/RegConsoleCmd <code>RegConsoleCmd</code>] 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.
+
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>public Action SayHello(int client, int argc) {
+
<pre>Action SayHello(int client, int argc) {
 
</pre>
 
</pre>
<code>SayHello</code> is a user-defined function that follows the ''prototype'' defined by [https://sm.alliedmods.net/new-api/console/ConCmd <code>ConCmd</code>]. 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 &quot;function prototypes do not match&quot;, which, to most newer developers, is a familiar compilation error message with words that don't have any sense to them.
+
<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 &quot;function prototypes do not match&quot;, 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 <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 143: Line 143:
 
<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>Action</code> 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 &quot;Unknown command&quot; if you don't return with that value.
+
<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 &quot;Unknown command&quot; if you don't return with that value.
  
 
= Marking Your Territory =
 
= Marking Your Territory =
Line 157: Line 157:
 
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.
  
Every entity has both ''netprops'' and ''datamaps''.
+
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.
 
* sendprops / netprops are properties that are sent over the network.
Line 171: Line 171:
 
</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>. This will provide you with a list of properties associated with each entity class.
 +
 
 +
{{Note|Some names refer to both netprops and dataprops.  In that case, using the netprop is preferred, as it will inform the engine that the change needs to be propagated to clients.}}
  
 
= Timers =
 
= Timers =
Line 187: Line 189:
 
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:
  
* <code>GetClientSerial</code> / <code>GetClientFromSerial</code> returns 0 if the client isn't valid anymore
+
* {{SourceMod API|file=clients|function=GetClientSerial}} / {{SourceMod API|file=clients|function=GetClientFromSerial}} returns 0 if the client isn't valid anymore
* <code>EntIndexToEntRef</code> / <code>EntRefToEntIndex</code> returns <code>INVALID_ENT_REF</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 <code>IsValidEntity</code>.
+
* {{SourceMod API|file=halflife|function=EntIndexToEntRef}} / {{SourceMod API|file=halflife|function=EntRefToEntIndex}} returns <code>INVALID_ENT_REF</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}}.
* <code>GetClientUserId</code> / <code>GetClientFromUserId</code> also returns 0 if the client isn't valid anymore. The values start at 1 and are incremented on player connections and map changes.
+
* {{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 197:
 
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.  Work on some real examples.}}
+
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 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 {{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));
 +
 
 +
    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.  Work on a <code>DataPack</code> example as a follow-up to the timer example above.}}
  
 
For other examples, see the [[Handles (SourceMod Scripting)|Handles page]] on the AlliedModders wiki.
 
For other examples, see the [[Handles (SourceMod Scripting)|Handles page]] on the AlliedModders wiki.
Line 211: Line 249:
 
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 <code>PrintToServer</code> to print debug their code of runtime errors.  Some tips:
+
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".

Revision as of 12:39, 23 February 2021

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);
}

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.

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

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.
  • 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. This will provide you with a list of properties associated with each entity class.

Note:Some names refer to both netprops and dataprops. In that case, using the netprop is preferred, as it will inform the engine that the change needs to be propagated to clients.

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:

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() {
    RegConsoleCmd("sm_delayedecho", DelayedEcho);
}

Action DelayedEcho(int client, int argc) {
    char message[64];
    GetCmdArgString(message, sizeof(message));

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