Difference between revisions of "Metamod:Source Development"

From AlliedModders Wiki
Jump to: navigation, search
(SourceMM 1.2.2 and manual callclasses)
m (Half-Life 2: Deathmatch now uses the Orange Box engine.)
 
(43 intermediate revisions by 7 users not shown)
Line 1: Line 1:
This article is an introduction to [[SourceMM]] coding.   
+
This article is an introduction to coding for Metamod:Source.  The main article of importance is [[SourceHook Development]].  Additional information can be found in the sample and stub plugins found in the Metamod:Source source code distribution.
  
{{qnotice|The majority of this documentation should be merged or moved into an article about [[SourceHook]].}}
+
If you find this article to be a tough read, you may want to skip over to the [[Sample Plugins (Metamod:Source)|Sample Plugins]] article.  See also setting up a [[Metamod:Source Environment]]. Note that Metamod:Source is a C++ platform, and you should have an intermediate knowledge of C++ (or, at the least, a strong will to research what's in the SDK).
  
{{qnotice|This article probably needs some better formatting.}}
+
=Differences from Valve Plugins=
 +
A Valve Server Plugin has more out-of-box callbacks than a Metamod:Source plugin, but:
 +
*A Valve Server Plugin cannot opt-out of those callbacks.  They must be implemented, even if as empty functions.
 +
*A Valve Server Plugin cannot hook or override any function not explicitly marked as one of its callbacks.  It can do so by using low-level hacks, or by embedding SourceHook, but this sets up a conflict where plugins are vying to alter and override the same memory addresses.
  
=Requirements=
+
Thus, Metamod:Source has two goals:
You must have the Valve [[HL2SDK]], available from your [[Steam]] Menu. For Windows, you must have [[Microsoft Visual Studio]] 7.0 or 7.1 (we have not tried 6.0). For [[Linux]], Valve requires you use at least [[GCC]] 3.4.1. You should also have a copy of the [[Metamod:Source]] source code, available [http://www.sourcemm.net/ here] (sourcemm/sourcehook and sourcemm/sourcemm).
+
*Resolve the hooking conflicts by creating a centralized, run-time hooking system.
 +
*Create a standardized, easy, and flexible API to abstract the process of hooking functions.
  
For [[Microsoft Visual Studio]], you should make sure that you have the following HL2SDK paths in your include settings (Tools -> Options -> Projects, VC++ Directories, Include Files), as well as the "sourcehook" and "sourcemm" folders:
+
As an example of this, a Valve Server Plugin has about 13 callbacks, 3 of which can be overridden.  A Metamod:Source plugin can safely hook any virtual function in the SDK, and override/supersede each of them as it pleases.
*dlls
 
*public
 
*public\vstdlib
 
*public\tier1
 
*public\tier0
 
*public\engine
 
*public\dlls
 
*tier1
 
*lib\public (this should be in your Library Paths!)
 
  
=Plugin API=
+
While a stub Metamod:Source plugin may look bare, it is easy to re-implement the functionality provided by <tt>IServerPluginCallbacks</tt>, and this is done so in the [[Sample Plugins (Metamod:Source)|Sample Plugins]].
In order to write a plugin, you must implement the ISmmPlugin interface, similar to IServerPluginCallbacks. Each Metamod:Source release has a minimum required interface version and a current version. The minimum version is guaranteed to be upward compatible to the current, however, it may be lacking some features.
 
  
Once you've implemented the interface, you must also have a global singleton of your plugin available. There are a few macros to assist you in properly exposing the interface as a DLL and setting up the API states.
 
  
*{{bcode|PLUGIN_GLOBALVARS}}() - Place in header. Declares the global variables that some API calls require (such as g_SHPtr and g_PLAPI).
+
=Plugin API=
*{{bcode|PLUGIN_EXPOSE}}(class, singleton) - Place in .cpp file. Declares the external CreateInterface function which exposes the API.
+
Metamod:Source has two separate APIs:
*{{bcode|PLUGIN_SAVEVARS}}() - Use first thing in ISmmPlugin::Load(), saves the global variables sent from SourceMM.
+
*[http://www.metamodsource.net/dox-1.4.3/ 1.4 API], contained in <tt>core-legacy</tt>. This is the API you should use if you're building a plugin to run on the following Source engines:
 +
**Original (The Ship)
 +
**Episode 1 (Older third party mods).
 +
*[http://www.metamodsource.net/dox/ 1.6 API], contained in <tt>core</tt>. This is the API you should use if you're building against the following Source engines:
 +
**Orange Box (Garry's Mod, Newer third party mods)
 +
**Orange Box Valve (Team Fortress, Day of Defeat, CS:S, HL2:DM)
 +
**Left 4 Dead
 +
**Left 4 Dead 2
 +
**Alien Swarm
  
The actual plugin API you must implement as of this writing is version 004. To see a description of each of the functions, you can view the doxygen information here. Note that the Load, Unload, Pause, and Unpause functions allow you to refuse the action and copy an error message (you should check to make sure error is not NULL - it can be).
+
It is important to get this right. The two APIs are not compatible, and if you wish to write a plugin that work across all engines, you must take special care in your plugin as a few API calls were renamed along the way.
  
=SourceHook (Hooking)=
+
More information is available in the article [[MM:S API Differences]].
SourceHook is the engine used to intercept function calls, much like Metamod. The difference with SourceHook is that it can intercept any virtual function in any class that, at compile time, you have the header for. SourceHook has the following steps of operation:
 
  
* Declare the prototype of the function you are going to hook. This generates compile-time code that is able to pinpoint exactly how to go about hooking the function.
+
=Starting a plugin=
* Hook the function - as a member function of another class or a regular static function.
+
See [[Metamod:Source Environment]] to set up your build environment.
* Before the hooked function is called, all of the "pre" hook handlers attached to it are called. Each hook can set a special flag, the highest of which is chosen as a final operation. This flag specifies whether the original function should be called or not.
 
* Once all the hooks have been called, SourceHook decides whether to call the original function. Another set of hooks are called directly after, called "post" hook handlers. You can specify whether each hook is a post or pre hook - it simply changes whether it's called before or after the original call is made.
 
* After you are done using a hook, you can safely remove it.
 
  
For example, let's say you wanted to intercept log messages in VEngineServer. Observe the prototype:
+
In order to write a plugin, you must implement the ISmmPlugin interface, similar to IServerPluginCallbacks.  An example of implementing this can be seen in the <tt>stub_mm</tt> sample plugin.
<pre>
 
virtual void LogPrint( const char *msg ) = 0;
 
</pre>
 
This is a virtual, public function of a class we have an instance of (let's say in IVEngineServer *m_Engine) and that we have the header for. It can be intercepted.
 
  
The first step is to figure out how to declare its prototype to SourceHook. This function is void, and has one parameter. The declaration macro follows these formats:
+
Once you've implemented the interface, you must also have a global singleton of your plugin available. There are a few macros to assist you in properly exposing the interface as a DLL and setting up the API states.
  
*{{bcode|SH_DECL_HOOK}}n - n is the number of parameters
+
*{{bcode|PLUGIN_GLOBALVARS}}() - Place in header. Declares the global variables that some API calls require (such as g_SHPtr and g_PLAPI).
**The parameters are: Class name, member function name, attributes, overloaded?, the return type, and a list of the parameter types.
+
*{{bcode|PLUGIN_EXPOSE}}(class, singleton) - Place in .cpp file. Declares the external CreateInterface function which exposes the API.
*{{bcode|SH_DECL_HOOKn_void}} - n is the number of parameters
+
*{{bcode|PLUGIN_SAVEVARS}}() - Use first thing in ISmmPlugin::Load(), saves the global variables sent from SourceMM.
**_void specifies that the function does not return a value. The format is the same as above except the "return type" parameter is missing.
 
*'''Note:''' Not covered here are the SH_DECL_HOOKn[_void]_vafmt hooks. These can hook string-formattable variable argument lists. You do not pass the string format or ellipses parameter. SourceHook will automatically format the string for your hook.
 
  
Our macro will look like this:
+
Full documentation of each callback is available in the <tt>ISmmPlugin.h</tt> header file.
<pre>
 
SH_DECL_HOOK1_void(IVEngineServer, LogPrint, SH_NOATTRIB, 0, const char *);
 
</pre>
 
This line must appear outside of a function. It means "Declare a hook prototype for LogPrint in IVEngineServer, which is a void function that has one parameter, which is a const char *".
 
  
Next we must actually hook the function. You can do this wherever you want to begin the interception. The two macros for hooking look like this:
 
  
*{{bcode|SH_ADD_HOOK_STATICFUNC}}(class, memberfunction, instance_pointer, handler, post) Hooks a virtual function to a static/global function.
+
=SourceHook=
**class - The name of the class
+
Using SourceHook is fully covered in the [[SourceHook Development]] article.
**memberfunction - The name of the member function
 
**instance - A pointer to an instance of the class
 
**handler - Your function that will be called on hooking
 
**post - true for post operation, false for pre operation
 
*{{bcode|SH_ADD_HOOK_MEMFUNC}}(class, memberfunction, instance, myinstance, myfunction, post) Hooks a virtual function to a member function of another class.
 
**class - The name of the class
 
**memberfunction - The name of the member function
 
**instance - An pointer to an instance of the class
 
**myinstance - A pointer to an instance of the class that has the handler member function
 
**myfunction - The name of the handler member function in your class
 
**post - true for post operation, false for pre-operation
 
  
Let's continue with the example. To hook LogPrint to a function in your class, CMetaHooks (instance, g_MetaHooks), you would use:
 
<pre>
 
SH_ADD_HOOK_MEMFUNC(IVEngineServer, LogPrint, m_Engine, &g_MetaHooks, &CMetaHooks::LogPrint, false);
 
</pre>
 
To remove the hook (either once it will no longer be unused, or at unload time):
 
<pre>
 
SH_REMOVE_HOOK_MEMFUNC(IVEngineServer, LogPrint, m_Engine &g_MetaHooks, &CMetaHooks::LogPrint, false);
 
</pre>
 
Now, your function contents will look something like this:
 
<pre>
 
void CMetaHooks::LogPrint(const char *msg)
 
{
 
//Code here
 
RETURN_META(MRES_IGNORED);
 
}
 
</pre>
 
Note this return statement. This style of returning is similar to Metamod's, where you can set four different flags to indicate how you would like SourceHook to proceed. In Metamod, this return statement was required. In SourceMM, it is only required if you wish to set a return state other than MRES_IGNORED.
 
 
*{{bcode|MRES_IGNORED}} - No states were changed, act as though nothing happened. Original function is still called.
 
*{{bcode|MRES_HANDLED}} - Something changed, but the original function was still called. This can be used to tell another plugin that you did something.
 
*{{bcode|MRES_OVERRIDE}} - The original function will still be called, but your return value will override whatever it returns.
 
*{{bcode|MRES_SUPERCEDE}} - The original function is not called, and your return value will be what is returned to the caller.
 
 
Note, that if you need to return a value, there is another macro. For example:
 
<pre>
 
RETURN_META_VALUE(MRES_SUPERCEDE, value);
 
</pre>
 
This is required for non-void functions, athough the return value is ignored using MRES_IGNORED or MRES_HANDLED.
 
 
Alternatively, you can hook to static member functions which has a slightly easier syntax. The DECL_HOOK line does not change. To add the hook:
 
<pre>
 
SH_ADD_HOOK_STATICFUNC(IVEngineServer, LogPrint, m_Engine, LogPrint_handler, false);
 
</pre>
 
Removing the hook:
 
<pre>
 
SH_REMOVE_HOOK_STATICFUNC(IVEngineServer, LogPrint, m_Engine, LogPrint_handler, false);
 
</pre>
 
Declaring the handler:
 
<pre>
 
void LogPrint_handler(const char *msg)
 
{
 
//Code here
 
RETURN_META(MRES_IGNORED);
 
}
 
</pre>
 
Note that vafmt functions hooked with SourceHook assume that the vafmt is for a printf style string, and that the vafmt occurs at the end. When setting the parameters, you do not include the '...' notation, and SourceHook will vafmt the input string for you (meaning you also don't specify ... in your hooked function).
 
 
=Calling Original Functions=
 
Often it will be necessary for you to call a function that's hooked, however, you don't want the hooks to be included in the calling. For example, if you want to entirely supercede a function and call it yourself from within a hook. To do this, you must request a call class. This is similar to MDLL_x() from Metamod for Half-Life 1.
 
 
Continuing with the above example, let's say we want to supercede the original call and log a different message. Assume we also have the pointer m_Engine which is a <tt>IVEngineServer *</tt>.
 
<cpp>
 
SourceHook::CallClass<IVEngineServer> *Engine_CC = SH_GET_CALLCLASS(m_Engine);
 
 
SH_CALL(Engine_CC, &IVEngineServer::LogPrint)("This is a log message!\n");
 
 
//When you are done with the pointer..
 
SH_RELEASE_CALLCLASS(Engine_CC);
 
</cpp>
 
 
Because of the complex nature of inheritance, instance pointers, and vtables, this syntax can be quite daunting. You may wish to make macros for specific functions or classes you use quite often, to reduce the amount of typing. For example:
 
<cpp>
 
//Assuming you have a global pointer g_Engine...
 
#define ENGCALL(func) SH_CALL(g_Engine, &IVEngineServer::func)
 
 
//Then you can do:
 
ENGCALL(LogPrint)("This is a test!");
 
</cpp>
 
 
The syntax of calling class construction is:
 
*<tt>SourceHook::CallClass<class> *ptr = SH_GET_CALLCLASS(instance);</tt>
 
**Returns an object which allows you to call the original function. Class is the name of the class which is the target, instance is an instance of that class.
 
*<tt>SH_CALL(cc_ptr, &class::func)([params])</tt>
 
**Pass the pointer returned from SH_GET_CALLCLASS as cc_ptr. The target function you call must be passed as a pointer-to-member-function, which takes the form &Class::Function as seen in previous examples. You must then complete the function call by adding a parenthetical parameter expression, even for void functions, which would be ().
 
 
=Other Macros=
 
All of the macros take g_PLAPI as the first parameter. For more information on this, see the global variables section.
 
  
 +
=Various Macros=
 
*{{bcode|META_CONPRINT}}(const char *msg)
 
*{{bcode|META_CONPRINT}}(const char *msg)
 
*{{bcode|META_CONPRINTF}}(const char *fmt, ...)
 
*{{bcode|META_CONPRINTF}}(const char *fmt, ...)
**These two functions are recommended over Msg() for printing to the server console. Msg() does not relay commands back through rcon, and as of this writing Valve does not expose the function which does. To be able to display text through the rcon console (much like HL1), you should use these functions. If SourceMM is unable to extract the function properly, it will automatically default to Msg.
+
**These two functions are equivalent to ConMsg().
 
*{{bcode|META_LOG}}(g_PLAPI, const char *msg, ...)
 
*{{bcode|META_LOG}}(g_PLAPI, const char *msg, ...)
 
**Logs a message through IVEngineServer::LogPrint(). A newline is automatically added, and msg is formatted as a sprintf() style string. Logging is done by the game server and can be enabled by adding ''log on'' to server.cfg or typing it in the console. The log files are found in the logs directory of the particular MOD you are running.
 
**Logs a message through IVEngineServer::LogPrint(). A newline is automatically added, and msg is formatted as a sprintf() style string. Logging is done by the game server and can be enabled by adding ''log on'' to server.cfg or typing it in the console. The log files are found in the logs directory of the particular MOD you are running.
 
*{{bcode|META_REGCVAR}}(var)
 
*{{bcode|META_REGCVAR}}(var)
**Registers a ConCommandBase pointer through SourceMM. The correct way to use this is to create an IConCommandBaseAccessor, and inside RegisterConCommandBase, call the macro on the given ConComandBase. This will ensure that SourceMM correctly detects and unloads your cvars and concommands at the appopriate time. Otherwise, unloading your plugin will cause a crash.
+
**Registers a ConCommandBase pointer through Metamod:Source.
 
*{{bcode|META_UNREGCVAR}}(var)
 
*{{bcode|META_UNREGCVAR}}(var)
**Unregisters a ConCommandBase pointer. This is not needed if you have set up your IConCommandBaseAccessor correctly (and called ConCommandBaseMngr::OneTimeInit()).
+
**Unregisters a ConCommandBase pointer.
 +
 
  
=Events System=
+
=Metamod Events=
The Events System is based on IMetamodListener. By implementing the IMetamodListener class and using g_SMAPI->AddListener, you can watch for certain, low-traffic events. These events are split into three categories:
+
The Metamod Events System is based on IMetamodListener. By implementing the IMetamodListener class and using g_SMAPI->AddListener, you can watch for certain, low-traffic events. These events are split into three categories:
  
 
*Plugin Events let you listen for plugin pauses and unloads. This is important if you're relying on information from another plugin, as you can handle cases where a live plugin has become invalid.
 
*Plugin Events let you listen for plugin pauses and unloads. This is important if you're relying on information from another plugin, as you can handle cases where a live plugin has become invalid.
*Game Events are simple events that SourceMM is already hooking and makes available. These are LevelShutdown and LevelInit right now.
+
*Game Events are simple events that Metamod:Source is already hooking and makes available. These are LevelShutdown and LevelInit right now.
 
*Query Events occur when a factory is used. The four main factories (Engine, GameDLL, FileSystem, and Physics) are all overridable. You should simply return a non-NULL result to override, and fill the return code with IFACE_OK if available. There is no way to handle the case of two plugins overriding right now. The final factory is the Metamod Factory, which is the factory that Metamod:Source adds to the runtime space for plugins. By default, it only contains the interfaces for the PluginManager and SourceHook. Plugins can use this to add new interfaces. Other plugins request these interfaces through g_SMAPI->MetaFactory().
 
*Query Events occur when a factory is used. The four main factories (Engine, GameDLL, FileSystem, and Physics) are all overridable. You should simply return a non-NULL result to override, and fill the return code with IFACE_OK if available. There is no way to handle the case of two plugins overriding right now. The final factory is the Metamod Factory, which is the factory that Metamod:Source adds to the runtime space for plugins. By default, it only contains the interfaces for the PluginManager and SourceHook. Plugins can use this to add new interfaces. Other plugins request these interfaces through g_SMAPI->MetaFactory().
 +
  
 
=Global Variables=
 
=Global Variables=
Line 180: Line 81:
 
**The SourceHook::ISourceHook * pointer to SourceHook's interface.
 
**The SourceHook::ISourceHook * pointer to SourceHook's interface.
 
*{{bcode|g_SMAPI}}
 
*{{bcode|g_SMAPI}}
**The ISmmAPI * pointer to SourceMM's interface.
+
**The ISmmAPI * pointer to Metamod:Source's interface.
 
 
=Compiling=
 
To see more about compiling, see the Sample Plugin Compiling section.
 
  
Modifying the default Makefiles for your own projects:
 
  
*<tt>OPT_FLAGS</tt> - Optimization flags
+
=Interface Searching=
*<tt>DEBUG_FLAGS</tt> - Debug flags
+
<tt>ISmmAPI::VInterfaceMatch()</tt> can be used for searching for an interface..  This simplified version corrects the design flaw in InterfaceSearch() whereby passing an unmodified INTERFACEVERSION string would only search interfaces later than or equal to that version.  For example, <tt>INTERFACEVERSION_SERVERGAMEDLL</tt> being "ServerGameDLL005" would not find a GameDLL using ServerGameDLL004. 
*<tt>CPP</tt> - C++ Compiler
 
*<tt>OBJECTS</tt> - List of C++ files to compile
 
*<tt>LINK</tt> - Linker Options
 
*<tt>CFLAGS</tt> - Base Compiler Flags
 
*<tt>BINARY</tt> - Output Binary Name
 
  
Makefile commands:
+
<tt>VInterfaceMatch()</tt> removes the "max" parameter from <tt>InterfaceSearch()</tt> and adds an optional "chop" parameter, which specifices whether or not the interface should be searched from the beginning (default) or from the current version.
  
*clean - Cleans all build files
 
*debug - Builds debug version
 
  
=1.1 Changes - Late Loading, Events=
+
=VSP Interface Hooking=
SourceMM 1.1 changed quite a few things internally, and externally made many breaking changes. These include:
+
Metamod:Source can provide a "virtual" [[Valve Server Plugin]] plugin interface. This is useful for providing an <tt>IServerPluginCallbacks</tt> pointer to certain routines, or hooking functions it has.
  
*ISmmPlugin::Load() has removed the factory list parameter (the factory system was replaced with the event system). Also, a boolean parameter was added, specifying whether the plugin was loaded late or not.
+
Example: (Note that GetVSPInfo is for Metamod:Source 1.6+)
*ISmmPlugin::Load() is now called BEFORE DLLInit(), rather than after. This means it might not have all information it needs -- for example, IGameEvents won't be parsed yet. You will need to do things like this in ISmmPlugin:AllPluginsLoaded() instead, as it is guaranteed to occur when all DLLs are initialized.
+
<cpp>SH_DECL_HOOK2(IServerPluginCallbacks, NetworkIDValidated, SH_NOATTRIB, 0, PLUGIN_RESULT, const char *, const char *);
*ISmmPlugin now has default functions -- you don't have to implement all of them.
 
*SourceHook was modified to use interfaces rather than straight struct pointers. This breaking changes will ensure that future SourceHook modifications do not break older plugins.
 
*SourceHook was modified to be re-entrant.
 
*SourceHook now has a tiny template library with it. This removes the necessity to link against libstdc++.so.*, which harms binary compatibility across linux distributions.
 
*SourceMM's GameDLL code was completely rewritten. It will now work with newer mods much easier, and issues like the DoD:S release will be much easier to deal with (if at all). It also removes a few important speed bottlenecks.
 
*An events system was added.
 
  
=1.2 Changes - Changing Parameters, Manual Hooking=
+
IServerPluginCallbacks *vsp_iface = NULL;
SourceMM 1.2 now supports changing the parameters in a hook chain. For example, say the GameDLL has a function called IGameDLL::PrintString(const char *str). You can hook this, let it continue, but change the "str" parameter passed in to the rest of the hooks and the GameDLL. To do this, use the following macros:
 
  
*<tt>RETURN_META_NEWPARAMS</tt>
+
bool Plugin::Load(PluginId id, ISmmAPI *ismm, char *error, size_t maxlen, bool late)
*<tt>RETURN_META_VALUE_NEWPARAMS</tt>
 
 
 
Example:
 
<cpp>
 
class ISomething
 
 
{
 
{
  virtual void Function1(int num) =0;
+
    if ((vsp_iface = ismm->GetVSPInfo(NULL)) == NULL)
  virtual bool Function2(bool what, int hi) =0;
+
    {
 +
      ismm->EnableVSPListener();
 +
      ismm->AddListener(this, this);
 +
    }
 
}
 
}
  
void MyHook1(int num)
+
void Plugin::OnVSPListening(IServerPluginCallbacks *iface)
 
{
 
{
  //if num is 0,  
+
    SH_ADD_HOOK_MEMFUNC(IServerPluginCallbacks, NetworkIDValidated, iface, &g_Plugin, &Plugin::NetworkIDValidated, false);
  // we will change the num parameter in the rest of the hooks, and the gamedll, to be 1.
+
    vsp_iface = iface;
  if (num == 0)
 
      RETURN_META_NEWPARAMS(MRES_IGNORED, &ISomething::Function1, (1))
 
  RETURN_META(MRES_SUPERCEDE);
 
 
}
 
}
  
bool MyHook2(bool what, int hi)
+
PLUGIN_RESULT Plugin::NetworkIDValidated(const char *pszUserName, const char *pszNetworkID)
 
{
 
{
  //change the "what" and "hi" parameters to be false and 3 respectively
+
    META_CONPRINTF("%s has been validated with Network ID %s\n", pszUserName, pszNetworkID);
  //also, return true, but specify that the value is ignored
+
    RETURN_META_VALUE(MRES_SUPERCEDE, PLUGIN_CONTINUE);
  RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, true, &ISomething::Function2, (false, 3));
+
}</cpp>
}
+
 
</cpp>
+
 
SourceHook also supports manual hooking of functions. This means you must know the virtual table index, the virtual table offset (for polymorphism), and the "this" pointer offset. Luckily, the polymorphic offsets are usually zero. This type of hook is ideal when supporting different ABI, for example, across different gamedlls. The API is almost identical:
+
=User Message Enumeration=
 +
API functions have also been added for the purpose of enumerating user messages. These serve to replace <tt>IServerGameDLL::GetUserMessageInfo()</tt> which can crash the server upon passing an invalid message index. The new functions include:  
 +
*<tt>GetUserMessageCount()</tt>
 +
*<tt>FindUserMessage()</tt>
 +
*<tt>GetUserMessage()</tt>
  
*<tt>SH_DECL_MANUALHOOKn[_void|_vafamt]</tt> -
+
Here is a quick example of how to use them:
**Declares the manual hook. A unique name must be given to each manual hook.
+
<cpp>// Get index of 'SayText' message
*<tt>SH_[ADD|REMOVE]_MANUALHOOK_[STATIC|MEM]FUNC</tt> -
+
int msgSayText = g_SMAPI->FindUserMessage("SayText");
**Adds or removes a static or member function hook on a manually declared hook.
 
*<tt>SH_MANUALHOOK_RECONFIGURE</tt> -
 
**Reconfigures the indexes and offsets of a manual hook.
 
  
An example is below, using the ISomething class from above.
+
// Get number of user messages in GameDLL
 +
int count = g_SMAPI->GetUserMessageCount();
  
<cpp>
+
const char *name;
SH_DECL_MANUALHOOK2(_M_Function1, 1, 0, 0, bool, bool, int);
+
int size;
  
void OnLoad(ISomething *pSomething)
+
// Print list of user message names and sizes
 +
for (int i = 0; i < count; i++)
 
{
 
{
//we reference the static function by its unique handle we gave it
+
    name = g_SMAPI->GetUserMessage(i, &size);
//the parameters are otherwise the same - this pointer, hook handler, and post/non-post
 
SH_ADD_MANUALHOOK_STATICFUNC(_M_Function1, pSomething, MyHook1, false);
 
}
 
  
void OnUnload(ISomething *pSomething)
+
    META_CONPRINTF("Message %d: (name \"%s\") (size %d)\n", i, name, size);
{
+
}</cpp>
SH_REMOVE_MANUALHOOK_STATICFUNC(_M_Function1, pSomething, MyHook1, false);
 
}
 
</cpp>
 
  
For more examples of the above features, you can look in SourceHook CVS's [http://www.tcwonline.org/cgi-bin/viewcvs.cgi/sourcemm/sourcehook/test/ test suite], which demonstrates a variety of hooking scenarios.
 
  
=1.2.2 Changes - Manual Callclasses=
+
=Compiling=
SourceMM 1.2.2 adds support for manual callclasses. These can be used to call manually hooked functions without invoking the hook chain. Manual callclasses work just like other callclasses with the exception that the virtual table information must be known when creating them. Here is a quick example of how to create and use them:
+
==Linux==
<cpp>// Creating a manual callclass
+
See [[Sample Plugins (Metamod:Source)|Sample Plugins]] and [[Metamod:Source Environment]] for getting sample Makefiles and setting up your build environment.
SourceHook::ManualCallClass *mcc = SH_GET_CALLCLASS(interfacePtr, sizeof(void*));
 
  
// Assuming we have this manual hook
+
There are a few GCC flags that you should not remove, or if you're writing a new Makefile, you should consider using:
SH_DECL_MANUALHOOK0_void(Some_Hook, 1, 0, 0);
+
*<tt>-Wall</tt> and <tt>-Werror</tt> - These help ensure a minimal amount of accidental coding mistakes.  Unless you absolutely refuse to fix warnings, it is a good idea to make sure your code compiles flawlessly.  Note that Valve does not build with these options, and many mistakes are masked in the SDK because of it.  If you use Valve's SDK instead of our fixed copy, you will not be able to use <tt>-Werror</tt>.
 +
*<tt>-Wno-non-virtual-dtor</tt> - This is kind of a useless warning from GCC (not all classes need destructors).
 +
*<tt>-fno-exceptions</tt> - You should not use exceptions because this creates a <tt>libstdc++</tt> dependency.
 +
*<tt>-fno-strict-aliasing</tt> - By default, GCC does not allow certain type casts which were perfectly legal in previous versions, and are perfectly legal in almost every other C/C++ compiler.  These type casts are prevalent and without this option, GCC '''will''' generate incorrect code.
  
// We can call the original function like so
+
There are a few flags you should generally not add, ever:
SH_MCALL(mcc, Some_Hook)();
+
*<tt>-lstdc++</tt> (same as using <tt>g++</tt>) - Even if you use the exact same compiler build as Valve, linking against the <tt>libstdc++</tt> will be painful.  Not all Linux distributions use the same version, and in general ABI compatibility on this library is frequently broken.  You will find that users report load failures or even crashes, and the problem may be very difficult to trace.  As such, you should avoid using exceptions or any part of the Standard Template Library (STL).  Valve provides many abstractions that should suffice, and SourceHook provides some as well.
 +
**''Note:'' Statically linking to <tt>libstdc++</tt> is not an option.  It will cause many problems.
 +
*<tt>-m64</tt> - Don't try to build 64-bit software off the SDK.  Valve doesn't even have a 64-bit port of the server yet.
  
// And when you are done with the pointer, you release the callclass like any other
+
==Windows==
SH_RELEASE_CALLCLASS(mcc);
+
For Visual Studio, there are a few project options which are required.  All of these settings are in the "Project Properties" dialog (under the Project menu).
</cpp>
 
  
It is also possible to call a function without having a hook declaration first. In order to this, you need a MFP (member function pointer) that corresponds to the function you wish to call. The class does not matter, but SourceHook::EmptyClass can be used for this purpose. For example, if the function takes two parameters (a const char * and and bool) and returns an int, you would do the following:
+
*Configuration Properties -> General
<cpp>// Member function pointer typedef
+
**"Character Set" should be "Use Multi-Byte Character Set," '''not''' Unicode.
typedef int (SourceHook::EmptyClass::*MFP_MyFunc)(const char *, bool);
+
*Configuration Properties -> C/C++
 +
**General
 +
***"Detect 64-bit Portability Issues" should be "No," unless you like (mostly) pointless warnings.
 +
**Preprocessor
 +
***"Processor Defines" -- You should add <tt>_CRT_SECURE_NO_DEPRECATE</tt> and <tt>_CRT_NONSTDC_NO_DEPRECATE</tt> to the semicolon-delimited list.  Microsoft decided to "deprecate" a bunch of Standard C API calls, apparently not noticing that people like to use them. Use these macros to stop the compiler from throwing a hissy-fit.
 +
**Code Generation
 +
***"Runtime" Library should be "Multi-threaded Debug" or "Multi-threaded" depending on the configuration.  You should not use the DLL version, as this will link against <tt>MSVCRT80[D]</tt>, which is part of the redistributable framework (and not packaged with Windows).
  
// Now to call the function, you do this
+
For sample linking and include folders, you should see [[Sample Plugins (Metamod:Source)]].  Sample Visual Studio project files are provided with everything you'd need.
int ret = SH_MCALL2(mcc, MFP_MyFunc(), 1, 0, 0)("hello", true);
 
</cpp>
 
  
 
[[Category:SourceHook]]
 
[[Category:SourceHook]]
[[Category:Documentation (SourceMM)]]
+
[[Category:Metamod:Source Development]]

Latest revision as of 11:00, 15 December 2010

This article is an introduction to coding for Metamod:Source. The main article of importance is SourceHook Development. Additional information can be found in the sample and stub plugins found in the Metamod:Source source code distribution.

If you find this article to be a tough read, you may want to skip over to the Sample Plugins article. See also setting up a Metamod:Source Environment. Note that Metamod:Source is a C++ platform, and you should have an intermediate knowledge of C++ (or, at the least, a strong will to research what's in the SDK).

Differences from Valve Plugins

A Valve Server Plugin has more out-of-box callbacks than a Metamod:Source plugin, but:

  • A Valve Server Plugin cannot opt-out of those callbacks. They must be implemented, even if as empty functions.
  • A Valve Server Plugin cannot hook or override any function not explicitly marked as one of its callbacks. It can do so by using low-level hacks, or by embedding SourceHook, but this sets up a conflict where plugins are vying to alter and override the same memory addresses.

Thus, Metamod:Source has two goals:

  • Resolve the hooking conflicts by creating a centralized, run-time hooking system.
  • Create a standardized, easy, and flexible API to abstract the process of hooking functions.

As an example of this, a Valve Server Plugin has about 13 callbacks, 3 of which can be overridden. A Metamod:Source plugin can safely hook any virtual function in the SDK, and override/supersede each of them as it pleases.

While a stub Metamod:Source plugin may look bare, it is easy to re-implement the functionality provided by IServerPluginCallbacks, and this is done so in the Sample Plugins.


Plugin API

Metamod:Source has two separate APIs:

  • 1.4 API, contained in core-legacy. This is the API you should use if you're building a plugin to run on the following Source engines:
    • Original (The Ship)
    • Episode 1 (Older third party mods).
  • 1.6 API, contained in core. This is the API you should use if you're building against the following Source engines:
    • Orange Box (Garry's Mod, Newer third party mods)
    • Orange Box Valve (Team Fortress, Day of Defeat, CS:S, HL2:DM)
    • Left 4 Dead
    • Left 4 Dead 2
    • Alien Swarm

It is important to get this right. The two APIs are not compatible, and if you wish to write a plugin that work across all engines, you must take special care in your plugin as a few API calls were renamed along the way.

More information is available in the article MM:S API Differences.

Starting a plugin

See Metamod:Source Environment to set up your build environment.

In order to write a plugin, you must implement the ISmmPlugin interface, similar to IServerPluginCallbacks. An example of implementing this can be seen in the stub_mm sample plugin.

Once you've implemented the interface, you must also have a global singleton of your plugin available. There are a few macros to assist you in properly exposing the interface as a DLL and setting up the API states.

  • PLUGIN_GLOBALVARS() - Place in header. Declares the global variables that some API calls require (such as g_SHPtr and g_PLAPI).
  • PLUGIN_EXPOSE(class, singleton) - Place in .cpp file. Declares the external CreateInterface function which exposes the API.
  • PLUGIN_SAVEVARS() - Use first thing in ISmmPlugin::Load(), saves the global variables sent from SourceMM.

Full documentation of each callback is available in the ISmmPlugin.h header file.


SourceHook

Using SourceHook is fully covered in the SourceHook Development article.


Various Macros

  • META_CONPRINT(const char *msg)
  • META_CONPRINTF(const char *fmt, ...)
    • These two functions are equivalent to ConMsg().
  • META_LOG(g_PLAPI, const char *msg, ...)
    • Logs a message through IVEngineServer::LogPrint(). A newline is automatically added, and msg is formatted as a sprintf() style string. Logging is done by the game server and can be enabled by adding log on to server.cfg or typing it in the console. The log files are found in the logs directory of the particular MOD you are running.
  • META_REGCVAR(var)
    • Registers a ConCommandBase pointer through Metamod:Source.
  • META_UNREGCVAR(var)
    • Unregisters a ConCommandBase pointer.


Metamod Events

The Metamod Events System is based on IMetamodListener. By implementing the IMetamodListener class and using g_SMAPI->AddListener, you can watch for certain, low-traffic events. These events are split into three categories:

  • Plugin Events let you listen for plugin pauses and unloads. This is important if you're relying on information from another plugin, as you can handle cases where a live plugin has become invalid.
  • Game Events are simple events that Metamod:Source is already hooking and makes available. These are LevelShutdown and LevelInit right now.
  • Query Events occur when a factory is used. The four main factories (Engine, GameDLL, FileSystem, and Physics) are all overridable. You should simply return a non-NULL result to override, and fill the return code with IFACE_OK if available. There is no way to handle the case of two plugins overriding right now. The final factory is the Metamod Factory, which is the factory that Metamod:Source adds to the runtime space for plugins. By default, it only contains the interfaces for the PluginManager and SourceHook. Plugins can use this to add new interfaces. Other plugins request these interfaces through g_SMAPI->MetaFactory().


Global Variables

These global variables are saved by PLUGIN_EXPOSE and PLUGIN_SAVEVARS. They are declared with PLUGIN_GLOBALVARS.

  • g_PLAPI
    • ISmmPlugin * pointer to your global class singleton.
  • g_PLID
    • The internal PluginId of your plugin.
  • g_SHPtr
    • The SourceHook::ISourceHook * pointer to SourceHook's interface.
  • g_SMAPI
    • The ISmmAPI * pointer to Metamod:Source's interface.


Interface Searching

ISmmAPI::VInterfaceMatch() can be used for searching for an interface.. This simplified version corrects the design flaw in InterfaceSearch() whereby passing an unmodified INTERFACEVERSION string would only search interfaces later than or equal to that version. For example, INTERFACEVERSION_SERVERGAMEDLL being "ServerGameDLL005" would not find a GameDLL using ServerGameDLL004.

VInterfaceMatch() removes the "max" parameter from InterfaceSearch() and adds an optional "chop" parameter, which specifices whether or not the interface should be searched from the beginning (default) or from the current version.


VSP Interface Hooking

Metamod:Source can provide a "virtual" Valve Server Plugin plugin interface. This is useful for providing an IServerPluginCallbacks pointer to certain routines, or hooking functions it has.

Example: (Note that GetVSPInfo is for Metamod:Source 1.6+)

SH_DECL_HOOK2(IServerPluginCallbacks, NetworkIDValidated, SH_NOATTRIB, 0, PLUGIN_RESULT, const char *, const char *);
 
IServerPluginCallbacks *vsp_iface = NULL;
 
bool Plugin::Load(PluginId id, ISmmAPI *ismm, char *error, size_t maxlen, bool late)
{
    if ((vsp_iface = ismm->GetVSPInfo(NULL)) == NULL)
    {
      ismm->EnableVSPListener();
      ismm->AddListener(this, this);
    }
}
 
void Plugin::OnVSPListening(IServerPluginCallbacks *iface)
{
    SH_ADD_HOOK_MEMFUNC(IServerPluginCallbacks, NetworkIDValidated, iface, &g_Plugin, &Plugin::NetworkIDValidated, false);
    vsp_iface = iface;
}
 
PLUGIN_RESULT Plugin::NetworkIDValidated(const char *pszUserName, const char *pszNetworkID)
{
    META_CONPRINTF("%s has been validated with Network ID %s\n", pszUserName, pszNetworkID);
    RETURN_META_VALUE(MRES_SUPERCEDE, PLUGIN_CONTINUE);
}


User Message Enumeration

API functions have also been added for the purpose of enumerating user messages. These serve to replace IServerGameDLL::GetUserMessageInfo() which can crash the server upon passing an invalid message index. The new functions include:

  • GetUserMessageCount()
  • FindUserMessage()
  • GetUserMessage()

Here is a quick example of how to use them:

// Get index of 'SayText' message
int msgSayText = g_SMAPI->FindUserMessage("SayText");
 
// Get number of user messages in GameDLL
int count = g_SMAPI->GetUserMessageCount();
 
const char *name;
int size;
 
// Print list of user message names and sizes
for (int i = 0; i < count; i++)
{
    name = g_SMAPI->GetUserMessage(i, &size);
 
    META_CONPRINTF("Message %d: (name \"%s\") (size %d)\n", i, name, size);
}


Compiling

Linux

See Sample Plugins and Metamod:Source Environment for getting sample Makefiles and setting up your build environment.

There are a few GCC flags that you should not remove, or if you're writing a new Makefile, you should consider using:

  • -Wall and -Werror - These help ensure a minimal amount of accidental coding mistakes. Unless you absolutely refuse to fix warnings, it is a good idea to make sure your code compiles flawlessly. Note that Valve does not build with these options, and many mistakes are masked in the SDK because of it. If you use Valve's SDK instead of our fixed copy, you will not be able to use -Werror.
  • -Wno-non-virtual-dtor - This is kind of a useless warning from GCC (not all classes need destructors).
  • -fno-exceptions - You should not use exceptions because this creates a libstdc++ dependency.
  • -fno-strict-aliasing - By default, GCC does not allow certain type casts which were perfectly legal in previous versions, and are perfectly legal in almost every other C/C++ compiler. These type casts are prevalent and without this option, GCC will generate incorrect code.

There are a few flags you should generally not add, ever:

  • -lstdc++ (same as using g++) - Even if you use the exact same compiler build as Valve, linking against the libstdc++ will be painful. Not all Linux distributions use the same version, and in general ABI compatibility on this library is frequently broken. You will find that users report load failures or even crashes, and the problem may be very difficult to trace. As such, you should avoid using exceptions or any part of the Standard Template Library (STL). Valve provides many abstractions that should suffice, and SourceHook provides some as well.
    • Note: Statically linking to libstdc++ is not an option. It will cause many problems.
  • -m64 - Don't try to build 64-bit software off the SDK. Valve doesn't even have a 64-bit port of the server yet.

Windows

For Visual Studio, there are a few project options which are required. All of these settings are in the "Project Properties" dialog (under the Project menu).

  • Configuration Properties -> General
    • "Character Set" should be "Use Multi-Byte Character Set," not Unicode.
  • Configuration Properties -> C/C++
    • General
      • "Detect 64-bit Portability Issues" should be "No," unless you like (mostly) pointless warnings.
    • Preprocessor
      • "Processor Defines" -- You should add _CRT_SECURE_NO_DEPRECATE and _CRT_NONSTDC_NO_DEPRECATE to the semicolon-delimited list. Microsoft decided to "deprecate" a bunch of Standard C API calls, apparently not noticing that people like to use them. Use these macros to stop the compiler from throwing a hissy-fit.
    • Code Generation
      • "Runtime" Library should be "Multi-threaded Debug" or "Multi-threaded" depending on the configuration. You should not use the DLL version, as this will link against MSVCRT80[D], which is part of the redistributable framework (and not packaged with Windows).

For sample linking and include folders, you should see Sample Plugins (Metamod:Source). Sample Visual Studio project files are provided with everything you'd need.