Writing Extensions
SourceMod's Extension System is a powerful way to greatly enhance the flexibility of your SourceMod plugins. You can take full advantage of SourceMod's Core, using the interfaces it provides, to create C++ plugins, plugin events, plugin native functions, and shared interfaces. Extensions can also hook into Metamod:Source.
This article goes into the details of creating a SourceMod Extension. Knowledge of C++ is required.
Contents
SDK Structure
First, download the SourceMod SDK from the SourceMod github, and one of the SDKs from the hl2sdk github. The directory structure should contain these folders:
- hl2sdk-something - One of the HL2 SDKs
- sourcemod-version - The sourcemod package
- extensions - Sourcemod Extensions code
- curl - Source code to the cURL extension
- geoip - Source code to the GeoIP extension
- mysql - Source code to the MySQL extension
- plugins - Source code to all of SourceMod's plugins
- include - The include files which document plugin API
- public - Interface files for SourceMod's Core Interfaces
- extensions - Interfaces that are provided by extensions
- sample_ext - The Sample Extension SDK
- extension.cpp - Sample C++ plugin
- extension.h - Sample C++ header file
- Makefile - Sample C++ plugin compiler
- smsdk_config.h - Sample C++ plugin settings
- sourcepawn - The include/interface files for SourcePawn
- compiler - The SourcePawn Compiler
Starting an Extension
For simplicity, this article will assume you are using the SDK base files. Navigate to extensions/public/sample_ext
, and ensure that you can compile your code. If using the command line, make
should do this, if using Visual Studio, see Setting up Visual Studio.
The first step of creating your extension is to configure it. Open the smsdk_config.h file and edit each of the following defines. Use the information below to guide you.
- SMEXT_CONF_NAME - The general name for your Extension (should be short).
- SMEXT_CONF_DESCRIPTION - A short description of what your extension does.
- SMEXT_CONF_VERSION - A version number string. By convention, SourceMod uses the four set build number notation, so build 1.2.3.4 means:
- 1 is the "Major" release version.
- 2 is the "Minor" release version.
- 3 is the "Maintenance" or "Revision" version.
- 4 is the "Build," usually an internal source control number.
- SMEXT_CONF_AUTHOR - Your name (or whoever/whatever authored the plugin).
- SMEXT_CONF_URL - The URL/homepage of this extension.
- SMEXT_CONF_LOGTAG - The logtag your extension will use for SourceMod logging. By convention, try to keep this under ten characters or so, and to make it all caps.
- SMEXT_CONF_LICENSE - Right now, the SourceMod Dev Team mandates that 3rd party extensions must be under an Open Source license. Putting a license abbreviation or very short message here is appropriate. For more information, see the SourceMod license.
- SMEXT_CONF_DATESTRING - You do not need to modify this.
If your plugin requires using SourceHook or Metamod:Source, uncomment this line:
#define SMEXT_CONF_METAMOD
Next, you should change the name of your extension. By convention, your extension's class name should start with a capital letter.
- First, open
extension.h
and change "Sample" in this line:
class Sample : public SDKExtension
- Next, open
extension.cpp
change these two lines:
Sample g_Sample; SMEXT_LINK(&g_Sample);
Visual Studio Users
Make sure you change the name of your .dll file. The naming convention for SourceMod extensions is name.ext.dll, where name is a short name for your extension. You are free to change this, but it is highly recommended that you keep the .ext.dll intact.
You can do this by going to Project -> Properties -> Configuration Properties -> Linker -> General. Set the Output File option appropriately.
Linux Users
Simply edit the Makefile and change the BINARY line near the top. Make sure you do not add any trailing spaces at the end of the name, or the build won't come out right.
You should now be able to compile a blank extension!
Creating Native Functions
Main article: Natives (SourceMod Development)
Most of the time, an extension exists to add "native functions" to the plugin system. Native functions are simply the functions that plugins can use in SourceMod. They are coded in C++ and have a short prototype declaration in an include file.
Prototyping
The first step to creating a native is to spec it, or prototype it. This is a simple but often time consuming process, but if you get in the habit of it, your documentation will be much better. Let's first create a very simply native. Here is a prototype for it, which we will place in sample.inc:
/** * Returns the square of a number. * * @param num Number to square. * @return The square of num. */ native int SquareNumber(num);
The native keyword tells the compiler that this function exists in an outside source. The comment style is similar to doxygen or Javadoc, and is preferred by the SourceMod Development Team.
Implementing
Now that our function is documented, it's time to create it! Each native function is bound to a C++ function. The prototype for this function looks like:
cell_t Function(IPluginContext *pContext, const cell_t *params);
A quick explanation of these types:
- cell_t - This is a 32bit signed integer, the generic data type for Pawn.
- IPluginContext - This interface provides Virtual Machine functions for retrieving or modifying memory in the plugin.
- params - This is the "stack" of parameters that the plugin passed. It is an array from [0..N] where 0 contains N. This means params[0] contains the number of arguments passed to the native.
- For example, if one parameter was passed, and the parameter is 25:
- params[0] == 1
- params[1] == 25
- Even though it tells you how many parameters are passed, you do not need to verify this number. The compiler will always pass the correct number of parameters in. The only time you need to verify it is for backwards compatibility or variable parameter lists, which will be discussed later.
- For example, if one parameter was passed, and the parameter is 25:
Now, let's write our native function:
cell_t SquareNumber(IPluginContext *pContext, const cell_t *params) { cell_t number = params[1]; return number * number; }
Binding
Now, the final step is to bind our native. Natives are bound in native lists. Each list must be terminated by a NULL bind. Example:
const sp_nativeinfo_t MyNatives[] = { {"SquareNumber", SquareNumber}, {NULL, NULL}, };
The first column/member is a string containing the name you've assigned to the native. The second column/member is the C++ function you're binding to. Here, both contain the same name, but it is certainly possible to bind a function to any valid Pawn function name, and likewise, you can bind one function to more than one name.
Lastly, you must tell Core about your native list. There are two places that are good to do this. Whichever you choose, you must uncomment the function in extension.h and implement it in extension.cpp.
- SDK_OnLoad - This is called when your extension is first loaded. If you do not require any outside interfaces, it is safe to add natives here.
- SDK_OnAllLoaded - This is called when all extensions have been loaded. This is generally a better place to add natives.
Let's say we choose SDK_OnAllLoaded, we'd then have code like this:
void Sample::SDK_OnAllLoaded() { sharesys->AddNatives(myself, MyNatives); }
Note: myself is a global variable declared in the SDK. It is a pointer that identifies your extension.
For more information on writing natives, see Natives (SourceMod Development).
Creating Events/Forwards
Events are an integral part to SourceMod. An Event is formally called a Forward. Forwards are functions inside a plugin which are not called by the plugin itself, but are triggered from an external source. For example, OnClientConnect is a forward which is triggered when a player connects to a server.
There are two major types of forwards:
- Managed: This forward is global and has a name. To use this forward, the function must be declared as public inside the user's script. Any plugin having this public function will receive the event.
- Unmanaged: This forward is private in nature. For example, the HookUserMessage native lets users specify a function to be called when a specific user message is sent. This is done with an unmanaged forward, and adding functions to this forward is not automatic. In general, function calls for unmanaged forwards are not public, although they can be.
Note: To enable the Forward interface, you must uncomment the definition SMEXT_ENABLE_FORWARDSYS in smsdk_config.h.
Managed Forwards
Let's say we want to create a forward that will be called when a player uses say chat. For simplicity, let's assume you already have a function that tells you when a player says say chat. We will just be creating the forward for it.
As we did before, first let's prototype our global forward. In our include file, we add this:
/** * @brief Called whenever a client says something in chat. * * @param client Index of the client. * @param text String containing the say text. * @return Plugin_Handled to block text from printing, Plugin_Continue otherwise. */ forward Action OnPlayerSayChat(int client, const char[] text);
The forward keyword tells the compiler that any function having this name must be declared exactly the same. In a plugin, this would
The next step is to declare your forward somewhere at the top of your extension.cpp file. This will look like:
IForward *g_pSayChat = NULL
Next, re-using our code from above, let's create this forward in SDK_OnAllLoaded:
void Sample::SDK_OnAllLoaded() { sharesys->AddNatives(myself, MyNatives); g_pSayChat = forwards->CreateForward("OnPlayerSayChat", ET_Event, 2, NULL, Param_Cell, Param_String); }
Explanation of parameters:
- "OnPlayerSayChat" - The name of our forward.
- ET_Event - Since forwards can execute multiple functions in multiple scripts, this is one of the ways of specifying what to do with the return value of each function. ET_Event means "return the highest value, do not allow stops." A stop refers to Pl_Stop, which lets a plugin block the rest of the event chain.
- 2 - This is the number of parameters our forward will have.
- NULL - Not used in our example. This lets you pass parameters by array rather than variable arguments.
- Param_Cell - The first parameter is a cell (integer).
- Param_String - The second parameter is a string.
Now, we write a quick function in our module to call this forward:
/** Fires the say chat event in plugins. Returns true to allow the text, false to block. */ bool FireChatEvent(int client, const char *text) { if (!g_pSayChat) { return true; } cell_t result = 0; g_pSayChat->PushCell(client); g_pSayChat->PushString(text); g_pSayChat->Execute(&result); if (result == Pl_Handled) { return false; } return true; }
As you can see, this was pretty simple. Each parameter is "pushed" one at a time, in ascending order. First we push the client index as a cell, then the text as a string. Once done, we execute, and the value will be returned by reference.
Lastly, there is a caveat. Unlike natives, SourceMod does not automatically clean up forwards for us when are done. We have to make sure we destroy them. Uncomment SDK_OnUnload from extension.h and implement it like so:
void Sample::SDK_OnUnload() { forwards->ReleaseForward(g_pSayChat); }
Unmanaged Forwards
Now things get a bit more complicated. However, unmanaged forwards are essentially the same -- they just give you more flexibility. Let's consider the managed example with a new twist. We want to let users add and remove hooks from their plugins, rather than having one global forward. The standard way to do this is with function IDs.
Example prototype:
typedef SayChatHook = function Action (int client, const char[] text); native void AddSayChatHook(SayChatHook hook); native void RemoveSayChatHook(SayChatHook hook);
The funcenum syntax lets you define all the valid prototypes for your hook. In this example, the only valid method is a function declared like this (only MyHook can be changed):
Action MyHook(int client, const char[] text)
How do we make such a beast? The first step is setting up our forward.
IChangeableForward *g_pSayChat = NULL; /* ... */ void Sample::SDK_OnAllLoaded() { g_pSayChat = forwards->CreateForwardEx(NULL, ET_Hook, 2, NULL, Param_Cell, Param_String); }
Notice two big differences. We now use IChangeableForward instead of IForward, and we use CreateForwardEx instead. The initial first parameter specifies a name for our forward - we're going to ignore this.
Now, we need to create our natives. These will be fairly simple:
cell_t AddSayChatHook(IPluginContext *pContext, const cell_t *params) { g_pSayChat->AddFunction(pContext, static_cast<funcid_t>(params[1])); return 1; } cell_t RemoveSayChatHook(IPluginContext *pContext, const cell_t *params) { IPluginFunction *pFunction = pContext->GetFunctionById(static_cast<funcid_t>(params[1])); g_pSayChat->RemoveFunction(pFunction); return 1; }
Note: These natives must be added -- that step is removed here and explained earlier.
You do not need to worry about a plugin unloading - Core will automatically remove the functions for you. Since an IChangeableForward is also an IForward, the FireChatEvent function from earlier does not need to change. We're done!
Creating Interfaces
Do you want your extension to share an interface? If so, first you should create an interface file. This file should contain everything your interface needs - this way other authors can easily reference it. Your interface must inherit the SMInterface class. It must also declare two global macros that uniquely identify your interface. It must also implement two functions: GetInterfaceName and GetInterfaceVersion.
The two defines must start with SMINTERFACE_ and end with _NAME and _VERSION respectively. The first must be a string and the second must be an unsigned integer. This integer represents your interface's version number. While you are free to use it however you please, by default it should be linearly increased. If you make breaking changes or change the versioning scheme, you must overload the IsVersionCompat function in SMInterface.
Example:
#ifndef _INCLUDE_MYINTERFACE_FILE_H_ #define _INCLUDE_MYINTERFACE_FILE_H_ #include <IShareSys.h> #define SMINTERFACE_MYINTERFACE_NAME "IMyInterface" #define SMINTERFACE_MYINTERFACE_VERSION 1 class IMyInterface : public SourceMod::SMInterface { public: virtual const char *GetInterfaceName() { return SMINTERFACE_MYINTERFACE_NAME; } virtual unsigned int GetInterfaceVersion() { return SMINTERFACE_MYINTERFACE_VERSION; } public: /** * @brief This function does nothing. */ virtual void DoNothingAtAll() =0; }; #endif //_INCLUDE_MYINTERFACE_FILE_H_
Notice, of course, that our interface is pure virtual. This means it must be implemented in the extension. Assuming you know C++, this should be fairly straight forward, so let's skip right to how to add this interface to the SourceMod shared system:
bool Sample::SDK_OnLoad(char *error, size_t err_max, bool late) { sharesys->AddInterface(myself, &g_MyInterface); return true; }
Note: We do this in SDK_OnLoad() instead of SDK_OnAllLoaded(). Otherwise, an extension might try to search for your interface during SDK_OnAllLoaded(), and it might fail depending on the load order.
That simple? Yup! Now other extensions can use your interface.
Using Interfaces/Other Extensions
There are a variety of interfaces that are available, but not loaded by default in the Extension SDK. These interfaces can be retrieved and used in your extension.
Core Interfaces
If they are provided by Core, getting them is very simple. Many interfaces can simply be uncommented in smsdk_config.h and they will become usable. See the "Available Interfaces" list below for the exposure list.
Otherwise, most of the .h interface files in the root of the public folder in the SDK are Core interfaces. For example, code to use the IPluginManager interface might look like this:
#include <IPluginSys.h> IPluginManager *g_pPlugins = NULL; bool Sample::SDK_OnLoad(char *error, size_t err_max, bool late) { SM_GET_IFACE(PLUGINSYSTEM, g_pPlugins); return true; }
Where did we get PLUGINSYSTEM from? Observe the two lines in IPluginSys.h:
#define SMINTERFACE_PLUGINSYSTEM_NAME "IPluginManager" #define SMINTERFACE_PLUGINSYSTEM_VERSION 1
The SM_GET_IFACE macro uses the text in between the SMINTERFACE_ and the _NAME characters. It also uses the version for compatibility checking. If it can't find the interface, it automatically prints to the error buffer and returns false.
Default Interfaces
There are interfaces which are grabbed by the Extension SDK by default. You do not need to query for them on load, or even check if they are valid or not. They are:
- IShareSys - Exposed as sharesys, found in IShareSys.h
- HANDLESYSTEM - Exposed as handlesys, found in IHandleSys.h
- SOURCEMOD - Exposed as g_pSM, found in ISourceMod.h
- FORWARDMANAGER - Exposed as forwards, found in IForwardSys.h
Available Interfaces
- SMEXT_ENABLE_FORWARDSYS: forwards
- SMEXT_ENABLE_HANDLESYS: handlesys
- SMEXT_ENABLE_PLAYERHELPERS: playerhelpers
- SMEXT_ENABLE_DBMANAGER: dbi
- SMEXT_ENABLE_GAMECONF: gameconfs
- SMEXT_ENABLE_MEMUTILS: memutils
- SMEXT_ENABLE_GAMEHELPERS: gamehelpers
- SMEXT_ENABLE_TIMERSYS: timersys
- SMEXT_ENABLE_THREADER: threader
External Interfaces
The situation changes if your extension requires another extension, since extensions may load in a completely random order. The first step is to mark the other extension as a dependency. Let's say you want to use the IThreader.h interfaces from threader.ext.dll. First, we declare it as such:
bool Sample::SDK_OnLoad(char *error, size_t err_max, bool late) { sharesys->AddDependency(myself, "thread.ext", true, true); return true; }
The two boolean parameters to AddDependency mean, respectively: "try to automatically load this extension" and "this extension cannot work without the dependency."
Second, we query for the interface in SDK_OnAllLoaded. Since we don't know when thread.ext will actually be loaded, we have to wait until everything is definitely loaded.
IThreader *g_pThreader = NULL; /* ... */ void Sample::SDK_OnAllLoaded() { SM_GET_LATE_IFACE(THREADER, g_pThreader); }
Third, we need to find a way to fail if this interface was never found. The way to do this is by uncommenting QueryRunning from extension.h and implementing it:
bool Sample::QueryRunning(char *error, size_t err_max) { SM_CHECK_IFACE(THREADER, g_pThreader); return true; }
When SourceMod queries your extension, the macro above will either validate the pointer or return false. If it returns false, SourceMod marks your extension as failed.
Caveats
NULL Interfaces
There are a few ways that external interfaces can make your code complicated. The first is simple but a common oversight. Don't assume interfaces will be okay. For example, say we want to create a thread once we get the IThreader interface. Our code should something like this:
void Sample::SDK_OnAllLoaded() { SM_GET_IFACE(THREADER, g_pThreader); if (QueryRunning(NULL, 0)) { g_pThreader->MakeThread(/* stuff */); } }
Note that we query ourself in order to find if it's safe to continue executing code. We could also have simply checked if g_pThreader is NULL, but self-querying has a bonus: if your extension continues to do things like hook events or add listeners, you will be preventing your extension from entirely initializing itself while one interface is bad. Always make sure you have sanity checks like this where needed.
Dynamic Unloading
The second major caveat is that extensions can be dynamically unloaded. If you specified yourself as having a dependency and that each dependency is required, you will have no problem. Your extension will be unloaded before the interface is removed, and you can clean up memory/hooks in your SDK_OnUnload() code. There are two situations where this is a different story.
Optional Interfaces
The first situation is if you are not requiring an extension that contains an interface. Now, when the extension unloads, your extension will not be unloaded first. This creates a small problem, as you must clean up without being unloaded. There are two functions you can implement in your extension class to resolve this, both documented in IExtensionSys.h:
- QueryInterfaceDrop - Allows you to request unloading when a specific interface is unloaded. This is the default behavior for all interfaces. If you want to block this functionality, continue reading.
- NotifyInterfaceDrop - This is called when an interface is dropped. If your plugin will not be unloaded, you can use this to clean up any resources on a specific interface being removed.
Circular Dependencies
The second situation is a bit more complicated. It is possible for two extensions to have circular dependencies. For example:
- Extension "A" requires extension "B."
- Extension "B" requires extension "A."
If either extension is unloaded, the opposite extension is unloaded normally. The first extension then receives the interface drop callbacks. In this situation, it is essential that the NotifyInterfaceDrop function be implemented and used with the circular interface. Otherwise, you risk crashing when your extension unloads (or at the very least, leaking memory). Since the actual drop order is undefined, it means both extensions must implement this function to be safe.
Automatic Loading
There are two types of automatic loading: Dynamic and Global. Dynamic loading means your extension is only loaded when a plugin requires it. Global loading means your extension loads no matter what.
Dynamic Autoloading
Dynamic loading requires that you create an include file for plugins. Plugins that include your file will automatically load your extension. This requires implementing the Extension structure found in core.inc. You must expose the structure as public, and the name must start with "__ext_".
/** * Do not edit below this line! */ public Extension __ext_geoip = { name = "GeoIP", file = "geoip.ext", #if defined AUTOLOAD_EXTENSIONS autoload = 1, #else autoload = 0, #endif #if defined REQUIRE_EXTENSIONS required = 1, #else required = 0, #endif };
Explanations:
- name: The name of your module as written in SMEXT_CONF_NAME.
- file: The platform-inspecific portion of your extension's file name.
- autoload: Specifies that your module should always dynamically autoload by default. You can tweak this to either never autoload or always autoload (without letting the user toggle).
- required: Specifies whether or not this extension is required by your plugin. If for some reason your extension fails to load (or is not found), the plugin will also fail to load. This is changeable if you wish to override the default behavior.
You can copy and paste this example, but remember to modify the following portions:
- __ext_geoip: Change the "geoip" portion to your own unique variable name.
- "GeoIP: Change the GeoIP portion to your extension's name. This should match the name you chose as SMEXT_CONF_NAME.
- "geoip.ext": Change this to your extension's file name, without the .dll/.so portion.
Global Autoloading
To have your extension always autoload, you must create a .autoload file in the extensions folder. For example, to have the threader.ext.dll or threader.ext.so extensions autoload, you'd create the following file in extensions: threader.autoload.
A few notes:
- This only works for extensions using the .ext.<platform> convention. SourceMod cuts off the ".autoload" portion of the file, then adds the appropriate extension just as if 'sm exts load' was used.
- The file does not need to contain anything, it simply needs to exist.
Conventions
Designing API/Natives
- Start interface names with the capital letter 'I'.
- Use a consistent naming convention. SourceMod uses Microsoft/Pascal naming. Avoid Java/Camel casing for both API and natives.
- When exposing interfaces, make sure your exposure defines start with SMINTERFACE_ and end with _NAME and _VERSION
External Naming
- Logtags (SMEXT_CONF_LOGTAG) should be short names (under 10 characters or so) in all capital letters.
- Your extension file name should never have _i486 or other unnecessary platform-specific notations in its name.
- Your extension file name should always end in .ext.so or .ext.dll depending on the platform. The .ext. is SourceMod convention.
Setting up Visual Studio
Note: It is recommended that you make a copy of the sample_ext project and modify that rather than create your own project, as it will set up most of this for you
Sample Project
Instead of manually creating a project, you can make a copy of the sample_ext project and modify it.
The sample_ext project assumes that it is located in a subdirectory inside the SourceMod public directory. To change this, modify all references to ..\.. to $(SOURCEMOD15)\public and specify the SOURCEMOD15 variable as mentioned in the Optional Environment variables.
The sample_ext projectuses the following environment variables to locate the various source code parts. To set environment variables:
Windows XP: Start -> Settings -> Control Panel -> System -> Advanced -> Environment Variables
Windows Vista/7: Start -> Control Panel -> System -> Advanced system properties -> Advanced -> Environment Variables
Environment Variables
The environment variables used are:
- MMSOURCE17: Path to MetaMod: Source source code for version 1.7 or newer. Used to compile SourceMod itself.
- MMSOURCE18: Path to MetaMod: Source source code for version 1.8 or newer. Used in SourceMod 1.4 projects.
- MMSOURCE19: Path to MetaMod: Source source code for version 1.9 or newer. Used in SourceMod 1.5 projects.
- HL2SDK: Path to the Episode 1 SDK
- HL2SDK-SWARM: Path to the Alien Swarm SDK
- HL2SDK-DARKM: Path to the Dark Messiah SDK
- HL2SDKCSGO: Path to the Counter-Strike: Global Offensive SDK
- HL2SDKCSS: Path to the Counter-Strike: Source SDK
- HL2SDKL4D: Path to the Left 4 Dead SDK
- HL2SDKL4D2: Path to the Left 4 Dead 2 SDK
- HL2SDKOB: Path to the Orange Box SDK (not Source 2009)
- HL2SDKOBVALVE: Path to the Orange Box Valve / Source 2009 SDK (Half-Life 2: Deathmatch, Day of Defeat: Source, Team Fortress 2)
Optional environment variables:
- 'SOURCEMOD15: Optional path to SourceMod 1.5 or newer source code
Manually configuring a project
Paths
Visual Studio 2003: Tools -> Options -> Projects, VC++ Directories
Visual Studio 2005: Tools -> Options -> Projects and Solutions -> VC++ Directories
Include Paths
- SourceMod only:
- sdk\public\extensions
- sdk\public\sourcepawn
- sdk\public
- SourceMM (Note that sourcemm might be 'trunk' for you):
- sourcemm\core
- sourcemm\core\sourcehook
- HL2SDK:
- game\shared
- public\vstdlib
- public\tier1
- public\tier0
- public\engine
- public
- dlls
Link Paths
- HL2SDK:
- lib\public for version 2005
- lib-vc7\public for version 2003
Compiler Options
Note: These options are set by default in the sample Extension SDK.
Note: These options should be set for every build (Release/Debug/others).
For VS 2005, goto Project->Properties.
- General
- Character Set: Use Multi-Byte Character Set
- C/C++
- General
- Detect 64-bit Portability Issues: No
- Preprocessor
- Preprocessor Defines: Add _CRT_SECURE_NO_DEPRECATE, _CRT_NONSTDC_NO_DEPRECATE, SOURCEMOD_BUILD and WIN32
- Code Generation
- Runtime Library: Multi-threaded or /MT (for Release), Multi-threaded Debug or /MTd (for Debug)
- General
- Linker
- General
- Output File: Change $(ProjectName) to $(ProjectName).ext, or use your own custom string.
- Input
- Additional Dependencies: tier0.lib tier1.lib vstdlib.lib (for SourceMM attached extensions only)
- General