Difference between revisions of "Introduction to SourceMod Plugins"

From AlliedModders Wiki
Jump to: navigation, search
(Declaration)
(Plugin Structure)
Line 17: Line 17:
 
name = "My First Plugin",
 
name = "My First Plugin",
 
author = "Me",
 
author = "Me",
description = "My first plugin ever"
+
description = "My first plugin ever",
 
version = "1.0.0.0",
 
version = "1.0.0.0",
 
url = "http://www.sourcemod.net/"
 
url = "http://www.sourcemod.net/"
}
+
};
  
 
public OnPluginStart()
 
public OnPluginStart()

Revision as of 15:06, 23 December 2007

This guide will give you a basic introduction to writing a SourceMod plugin. If you are not familiar with the SourcePawn language, it is recommended that you at least briefly read the Introduction to SourcePawn article.

For information on compiling plugins, see Compiling SourceMod Plugins. The author of this article uses Crimson Editor to write plugins. Other possibilities are PSPad, UltraEdit, Notepad++, TextPad, or any other text editor you're comfortable with.

Plugin Structure

Almost all plugins follow have the same three elements:

  • Includes - Allows you to access the SourceMod API, and if you desire, API from external SourceMod extensions/plugins.
  • Info - Public information about your plugin.
  • Startup - A function which performs start-up routines in your plugin.

A skeletal plugin structure looks like:

#include <sourcemod>
 
public Plugin:myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
};
 
public OnPluginStart()
{
}

The information portion is a special syntax construct. You cannot change any of the keywords, or the public Plugin:myinfo declaration. The best idea is to copy and paste this skeletal structure and modify the strings to get started.

Includes

Pawn requires include files, much like C requires header files. Include files list all of the structures, functions, callbacks, and tags that are available. There are three types of include files:

  • Core include files, which is sourcemod.inc and anything it includes. These are all provided by SourceMod's Core.
  • Extension include files, which if used, will add a dependency against a certain extension.
  • Plugin include files, which if used, will add a dependency against a certain plugin.

Include files are loaded using the #include compiler directive.


Commands

Our first example will be writing a simple admin command to slap a player. We'll continue to extend this example with more features until we have a final, complete result.

Declaration

First, let's look at what an admin command requires. Admin commands are registered using the RegAdminCmd function. They require a name, a callback function, and default admin flags.

The callback function is what's invoked every time the command is used. Click here to see its prototype. Example:

public OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
}
 
public Action:Command_MySlap(client, args)
{
}

Now we've successfully implemented a command -- though it doesn't do anything yet. In fact, it will say "Unknown command" if you use it! The reason is because of the Action tag. The default functionality for entering console commands is to reply that they are unknown. To block this functionality, you must return a new action:

public Action:Command_MySlap(client, args)
{
	return Plugin_Handled;
}

Now the command will report no error, but it still won't do anything.

Implementation

Let's decide what the command will look like. Let's have it act like the default sm_slap command:

sm_myslap <name|#userid> [damage]

To implement this, we'll need a few steps:

  • Get the input from the console. For this we use GetCmdArg().
  • Find a matching player. For this we use FindTarget().
  • Slap them. For this we use SlapPlayer(), which requires including sdktools, an extension bundled with SourceMod.
  • Respond to the admin. For this we use ReplyToCommand().

Full example:

#include <sourcemod>
#include <sdktools>
 
public Plugin:myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever"
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
}
 
public OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
}
 
public Action:Command_MySlap(client, args)
{
	new String:arg1[32], String:arg2[32]
	new damage
 
	/* Get the first argument */
	GetCmdArg(1, arg1, sizeof(arg1))
 
	/* If there are 2 or more arguments, and the second argument fetch 
	 * is successful, convert it to an integer.
	 */
	if (args >= 2 && GetCmdArg(2, arg2, sizeof(arg2)))
	{
		damage = StringToInt(arg2)
	}
 
	/* Try and find a matching player */
	new target = FindTarget(client, arg1)
	if (target == -1)
	{
		/* FindTarget() automatically replies with the 
		 * failure reason.
		 */
		return Plugin_Handled;
	}
 
	SlapPlayer(target, damage)
 
	new String:name[MAX_NAME_LENGTH]
 
	GetClientName(target, name, sizeof(name))
	ReplyToCommand(client, "[SM] You slapped %s for %d damage!", name, damage)
 
	return Plugin_Handled;
}

For more information on what %s and %d are, see Format Class Functions. Note that you never need to unregister or remove your admin command. When a plugin is unloaded, SourceMod cleans it up for you.

ConVars

ConVars, also known as cvars, are global console variables in the Source engine. They can have integer, float, or string values. ConVar accessing is done through Handles. Since ConVars are global, you do not need to close ConVar Handles (in fact, you cannot).

The handy feature of ConVars is that they are easy for users to configure. They can be placed in any .cfg file, such as server.cfg or sourcemod.cfg. To make this easier, SourceMod has an AutoExecConfig() function. This function will automatically build a default .cfg file containing all of your cvars, annotated with comments, for users. It is highly recommend that you call this if you have customizable ConVars.

Let's extend your example from earlier with a new ConVar. Our ConVar will be sm_myslap_damage and will specify the default damage someone is slapped for if no damage is specified.

new Handle:sm_myslap_damage = INVALID_HANDLE
 
public OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
 
	sm_myslap_damage = CreateConVar("sm_myslap_damage", "5", "Default slap damage")
	AutoExecConfig(true, "plugin_myslap")
}
 
public Action:Command_MySlap(client, args)
{
	new String:arg1[32], String:arg2[32]
	new damage = GetConVarInt(sm_myslap_damage)
 
	/* The rest remains unchanged! */

Showing Activity, Logging

Almost all admin commands should log their activity, and some admin commands should show their activity to in-game clients. This can be done via the LogAction() and ShowActivity2() functions. The exact functionality of ShowActivity2() is determined by the sm_show_activity cvar.

For example, let's rewrite the last few lines of our slap command:

	SlapPlayer(target, damage)
 
	new String:name[MAX_NAME_LENGTH]
 
	GetClientName(target, name, sizeof(name))
 
	ShowActivity2(client, "[SM] ", "Slapped %s for %d damage!", name, damage)
	LogAction(client, target, "\"%L\" slapped \"%L\" (damage %d)", client, target, damage)
 
	return Plugin_Handled;
}

Multiple Targets

To fully complete our slap demonstration, let's make it support multiple targets. SourceMod's targeting system is quite advanced, so using it may seem complicated at first.

The function we use is ProcessTargetString(). It takes in input from the console, and returns a list of matching clients. It also returns a noun that will identify either a single client or describe a list of clients. The idea is that each client is then processed, but the activity shown to all players is only processed once. This reduces screen spam.

This method of target processing is used for almost every admin command in SourceMod, and in fact FindTarget() is just a simplified version.

Full, final example:

#include <sourcemod>
#include <sdktools>
 
public Plugin:myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever"
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
}
 
public OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
 
	sm_myslap_damage = CreateConVar("sm_myslap_damage", "5", "Default slap damage")
	AutoExecConfig(true, "plugin_myslap")
}
 
public Action:Command_MySlap(client, args)
{
	new String:arg1[32], String:arg2[32]
	new damage = GetConVarInt(sm_myslap_damage)
 
	/* Get the first argument */
	GetCmdArg(1, arg1, sizeof(arg1))
 
	/* If there are 2 or more arguments, and the second argument fetch 
	 * is successful, convert it to an integer.
	 */
	if (args >= 2 && GetCmdArg(2, arg2, sizeof(arg2)))
	{
		damage = StringToInt(arg2)
	}
 
	/**
	 * target_name - stores the noun identifying the target(s)
	 * target_list - array to store clients
	 * target_count - variable to store number of clients
	 * tn_is_ml - stores whether the noun must be translated
	 */
	new String:target_name[MAX_TARGET_LENGTH]
	new target_list[MAXPLAYERS], target_count
	new bool:tn_is_ml
 
	if ((target_count = ProcessTargetString(
			arg1,
			client,
			target_list,
			MAXPLAYERS,
			COMMAND_FILTER_ALIVE, /* Only allow alive players */
			target_name,
			sizeof(target_name),
			tn_is_ml)) <= 0)
	{
		/* This function replies to the admin with a failure message */
		ReplyToTargetError(client, target_count);
		return Plugin_Handled;
	}
 
	for (new i = 0; i < target_count)
	{
		SlapPlayer(target_list[i], damage)
		LogAction(client, target_list[i], "\"%L\" slapped \"%L\" (damage %d)", client, target_list[i], damage)
	}
 
	if (tn_is_ml)
	{
		ShowActivity2(client, "[SM] ", "Slapped %t for %d damage!", target_name, damage)
	}
	else
	{
		ShowActivity2(client, "[SM] ", "Slapped %s for %d damage!", target_name, damage)
	}
 
	return Plugin_Handled;
}

Client and Entity Indexes

One major point of confusion with Half-Life 2 is the difference between the following things:

  • Client index
  • Entity index
  • Userid

The first answer is that clients are entities. Thus, a client index and an entity index are the same thing. When a SourceMod function asks for an entity index, a client index can be specified. When a SourceMod function asks for a client index, usually it means only a client index can be specified.

A fast way to check if an entity index is a client is checking whether it's between 1 and GetMaxClients() (inclusive). If a server has N client slots maximum, then entities 1 through N are always reserved for clients. Note that 0 is a valid entity index; it is the world entity (worldspawn).

A userid, on the other hand, is completely different. The server maintains a global "connection count" number, and it starts at 1. Each time a client connects, the connection count is incremented, and the client receives that new number as their userid.

For example, the first client to connect has a userid of 2. If he exits and rejoins, his userid will be 3 (unless another client joins in-between). Since clients are disconnected on mapchange, their userids change as well. Userids are a handy way to check if a client's connection status has changed.

SourceMod provides two functions for userids: GetClientOfUserId() and GetClientUserId().

Events

Events are informational notification messages passed between objects in the server. Many are also passed from the server to the client. They are defined in .res files under the hl2/resource folder and resource folders of specific mods. For a basic listing, see Source Game Events.

It is important to note a few concepts about events:

  • They are almost always informational. That is, blocking player_death will not stop a player from dying. It may block a HUD or console message or something else minor.
  • They always use userids instead of client indexes.
  • Just because it is in a resource file does not mean it is ever called, or works the way you expect it to. Mods are notorious at not properly documenting their event functionality.

An example of finding when a player dies:

public OnPluginStart()
{
   HookEvent("player_death", Event_PlayerDeath)
}
 
public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast)
{
   new victim_id = GetEventInt(event, "userid")
   new attacker_id = GetEventInt(event, "attacker")
 
   new victim = GetClientOfUserId(victim_id)
   new attacker = GetClientOfUserId(attacker_id)
 
   /* CODE */
}

Further Reading

For further reading, see the "Scripting" section at the SourceMod Documentation.