December 10, 2023
Source Engine plugins are usually written in C++, the language of the Source Engine. The SDK is in C++, and the plugin interface is very C++ specific. However, this isn’t stopping anyone from writing plugins in other languages anyway. This article will provide an explanation of how to do just that, along with explaining what we are doing as well as how to interact with the engine generally. I will be using C to explain things as we go along, as much of the syntax is similar to C++ and C syntax is something that most people already know. I will also assume that you also know how to build a shared library using whatever build system you prefer.
Source is a fairly modular engine, consisting of a good number of
different binaries, most of them located in the bin
folder
of your game installation. They talk to each other through what are
called interfaces. To accomplish this, each shared library
exports a function called CreateInterface
with the
following signature:
void *CreateInterface(const char*, int*);
This function function takes two arguments: the name of the interface, which usually consists of the name followed by a version number, and a pointer to write the optional return code. This is almost always a null pointer as you can just as easily just check if the return value is null.
This function returns pointers to interface objects, which are how the dynamically loaded objects export their functionality to the rest of the engine.
In the source engine, an interface is a singleton instance of a C++ abstract class. This means that it has member functions implemented virtually, which is crucial for how the interface works. Virtual functions are implemented Virtual functions are implemented by adding a pointer to a table of virtual functions before the data members of the class.
If you are looking at the engine code, the interface classes will
usually start with an I
, and the actual object returned
will usually be of a class with the same name, but with a C
instead of the I
.
Here’s what a basic interface would look like for our purposes:
struct IExampleInterface {
void **vtable;
...other entries here
}
Or, you could describe the vtable with a struct. This is useful to know exactly what function is being called in the code.
struct IExampleInterface {
void *struct ExampleInterface_vtable;
};
struct ExampleInterface_vtable {
void (__thiscall *do_something)(void *this);
}
A plugin acts the same way as any other engine binary. When the
engine loads a plugin, it calls the plugin’s
CreateInterface
function asking for a specific interface,
and from that function we will return our implementation of that
interface to handle common game events.
That being said, let’s look at the plugin interface.
abstract_class IServerPluginCallbacks{
public:
virtual bool Load(CreateInterfaceFn interfaceFactory,
);
CreateInterfaceFn gameServerFactoryvirtual void Unload(void);
virtual void Pause(void);
virtual void UnPause(void);
virtual const char *GetPluginDescription(void);
virtual void LevelInit(char const *pMapName);
virtual void ServerActivate(edict_t *pEdictList, int edictCount, int clientMax);
virtual void GameFrame(bool simulating);
virtual void LevelShutdown(void);
virtual void ClientActive(edict_t *pEntity);
virtual void ClientDisconnect(edict_t *pEntity);
virtual void ClientPutInServer(edict_t *pEntity, char const *playername);
virtual void SetCommandClient(int index);
virtual void ClientSettingsChanged(edict_t *pEdict);
virtual PLUGIN_RESULT ClientConnect(bool *bAllowConnect, edict_t *pEntity,
const char *pszName, const char *pszAddress, char *reject,
int maxrejectlen);
virtual PLUGIN_RESULT ClientCommand(edict_t *pEntity, const CCommand &args);
virtual PLUGIN_RESULT NetworkIDValidated(const char *pszUserName,
const char *pszNetworkID);
// Added with version 2 of the interface.
virtual void OnQueryCvarValueFinished(QueryCvarCookie_t iCookie,
edict_t *pPlayerEntity, EQueryCvarValueStatus eStatus,
const char *pCvarName, const char *pCvarValue);
// added with version 3 of the interface.
virtual void OnEdictAllocated( edict_t *edict );
virtual void OnEdictFreed( const edict_t *edict );
};
Realistically, most plugins do not have any meaningful implementation for most of these functions, but we do need to populate the vtable with real functions so the game does not crash when it tries to call them. Let’s go!
Keep in mind that nearly every Source Engine game is 32-bit, so when building the plugin make sure you are building a 32-bit shared object.
On Windows, you must use clang or gcc to compile the plugin, as
Microsoft’s MSVC compiler does not support the thiscall
calling convention on C functions. Clang is reccomended over gcc though
since it works a lot better on Windows.
To use thiscall on Windows and the default cdecl on Linux, we can define a macro:
#ifdef WIN32
#define VCALLCONV __attribute__((thiscall))
#else
#define VCALLCONV
#endif
Calling conventions are different schemes for how to pass and return arguments to functions in the generated assembly. They are outside the scope of this article.
Anyway, first write stub functions for each of the functions in the
class above. Keep in mind you can use a void pointer to take a pointer
of any type in without crashing. PLUGIN_RESULT
,
EQueryCvarValueStatus
, and QueryCvarCookie_t
are all int
s. Return 0
from the functions that
return PLUGIN_RESULT
.
Here’s what a few of the functions should look like:
bool VCALLCONV Load(void *this,
,
CreateInterfaceFn ifacefactory) {
CreateInterfaceFn gameserverfactoryreturn false; // returns false when the plugin has loaded successfully
}
char VCALLCONV *GetPluginDescription(void *this) {
return "Example plugin";
}
Next, let’s make our vtable and ‘class’ object.
struct CMyExamplePlugin {
void *const *const vtable;
}
static const void *vtable[] = {
(void*)&Load,
(void*)&Unload,
...etc // follow the order in the class definition above
}
struct CMyExamplePlugin plugin_object = { vtable };
Keep in mind that some functions are different in newer engine branches, but this setup should work for anything except Portal 2 or Alien Swarm as far as I know.
Now it is time to implement our own CreateInterface
.
Most Source games use interface version 3, so we will write our function
like this.
void *CreateInterface(const char *name, int *ret) {
if (!strcmp(name, "ISERVERPLUGINCALLBACKS003")) {
if (ret) *ret = 0;
return &plugin_object;
}
if (ret) *ret = 1;
return 0;
}
On Windows, this function needs to be marked as
__declspec(dllexport)
This can be done with a few
preprocessor directives again if you want to be cross platform.
Once you build your plugin, you can put the binary in the game
directory. This is the folder next to the actually engine
executable, like hl2
or portal
. You can then
load it with the plugin_load
command.