Skip to content

How to Plugin modules

Dreamy Cecil edited this page May 28, 2024 · 1 revision

Serious Sam Classics Patch implements a custom system of modular plugins that can be automatically loaded every time the patch is initialized. These plugins can be used to extend or modify many aspects of the game right on top of it, independent from traditional mods.

See ExamplePlugin repository for the example of a plugin library with descriptions of each event that plugins can handle.

Features and special techniques described here should be considered when developing custom user plugins.

Shell symbol registering

When registering custom shell symbols to use within your plugins, be aware that you cannot define variables for the symbols within the plugin's memory space.

Plugins are released from memory automatically right before cleaning up Serious Engine, which is supposed to save symbols marked as persistent into the Scripts/PersistentSymbols.ini. Since plugins are getting erased from memory, values of the plugin symbols become inaccessible, leading to crashes related to memory access violation upon quitting the game or tool applications, potentially messing up your game files.

Variable symbols

How to properly register plugin variables in console.

// DON'T do this (using local variable)
//   This stores the symbol in the global scope of the plugin's memory
static INDEX _bRedScreen = TRUE;

// DO this (using plugin symbol class)
//   This stores only a reference to the symbol in the global scope of the plugin's memory
static CPluginSymbol _psRedScreen(SSF_PERSISTENT | SSF_USER, INDEX(1));

// Registering inside module's entry point
MODULE_API void Module_Startup(void)
{
  // DON'T do this (using Serious Engine method)
  //   This declares the symbol directly in the shell with the pointer to the variable
  _pShell->DeclareSymbol("persistent user INDEX sam_bRedScreenOnDamage;", &_bRedScreen);

  // DO this (using plugin API method)
  //   This registers the symbol through the API and stores it in memory space of a library/executable that initialized the Core library
  _psRedScreen.Register("sam_bRedScreenOnDamage");
};

CPluginSymbol constructor arguments are as follows:

Argument Description Example
1. Symbol flags These are the flags used by the shell itself to see what this symbol is supposed to be. The flags may only be SSF_PERSISTENT (saves values in "Scripts/PersistentSymbols.ini" upon quitting the game), SSF_USER (listed in the console) and SSF_CONSTANT (values cannot be changed via the console/shell).
2. Default value The default value that's going to be set to the symbol upon registering. If it's been loaded from PersistentSymbols.ini, the value won't be rewritten upon registering it again. It can either be an INDEX, a FLOAT or a const char * value.

CPluginSymbol::Register() arguments are as follows:

Argument Description Example
1. Symbol name The name that will be used to access the symbol value, i.e. the console command name. "iMySymbol"
2. Pre-function Shell function to call before modifying the symbol (the only argument the C++ function under that shell symbol has is a pointer to the registered symbol value). Can be left empty ("") or unspecified altogether (only if post-function isn't specified). "MyPreFunc"
3. Post-function Shell function to call after modifying the symbol (the only argument the C++ function under that shell symbol has is a pointer to the registered symbol value). Can be left empty ("") or unspecified altogether. "MyPostFunc"

To retrieve a value from the symbol, use GetIndex(), GetFloat() or GetString() of CPluginSymbol, depending on which type the default value is. Calling the wrong method will cause undefined behavior!

Function symbols

Shell functions in code

How to properly define plugin functions for use by the shell.

// Shell function definition
static void SetHealth(SHELL_FUNC_ARGS)
{
  BEGIN_SHELL_FUNC;
  INDEX iHealth = NEXT_ARG(INDEX);

  // Gather all local players and change their health
  CDynamicContainer<CPlayerEntity> cenPlayers;
  IWorld::GetLocalPlayers(cenPlayers);

  FOREACHINDYNAMICCONTAINER(cenPlayers, CPlayerEntity, iten) {
    iten->SetHealth(iHealth);
    CPrintF(TRANS("Set %s^r health to %d\n"), iten->GetName(), iHealth);
  }
};

Note the usage of SHELL_FUNC_ARGS and BEGIN_SHELL_FUNC macros in the function. These are platform-specific macros for properly accessing arguments that are passed into the C++ function by the game shell.

It's done this way due to how arrays of arguments are being passed depending on the compiler used. For example, in engine versions 1.05 and 1.07 (built with MSVC6.0) the arguments can be accessed directly, like void MyFunction(INDEX iInteger, FLOAT fNumber, const CTString &strString), while in 1.10 (built with MSVC10.0 or newer) the arguments need to be accessed from a pointer to the array of arguments, like void MyFunction(void *pArgs). These macros take care of compiler-dependent behavior and make it work like in 1.10 engine version.

In order to get values of a specific type one after another, use NEXT_ARG() macro function as follows:

Desired type Macro function usage Description
INDEX INDEX i = NEXT_ARG(INDEX); Shell passes a raw integer value as an argument upon calling the function.
FLOAT FLOAT f = NEXT_ARG(FLOAT); Shell passes a raw float value as an argument upon calling the function.
CTString const CTString &str = *NEXT_ARG(CTString *); or CTString str = *NEXT_ARG(CTString *); Shell passes a pointer to a CTString as an argument upon calling the function.

Shell function registering

How to properly register plugin functions in console.

// Registering inside module's entry point
MODULE_API void Module_Startup(void)
{
  // DON'T do this (using Serious Engine method)
  _pShell->DeclareSymbol("user void cht_SetHealth(INDEX);", &SetHealth);

  // DO this (using plugin API method)
  GetPluginAPI()->RegisterMethod(TRUE, "void", "cht_SetHealth", "INDEX", &SetHealth);
};

CPluginAPI::RegisterMethod() arguments are as follows:

Argument Description Example
1. User visibility Marks the symbol function as "user", making it visible in the symbol list in the console upon typing a part of its name and pressing Tab. TRUE or FALSE
2. Function return type The value of which type will be returned from the function upon executing it. May only be "void", "INDEX", "FLOAT" or "CTString" (matching the C++ types).
3. Symbol name The name that will be used to call the function, i.e. the console command name. "MyCommand"
4. Function arguments Argument types that should be passed into the C++ function upon call. It can either be "void" for no arguments or a sequence of INDEX, FLOAT and CTString. "FLOAT", "INDEX, INDEX", "CTString, INDEX, FLOAT, FLOAT" etc.
5. Pointer to the function Here you specify a pointer to the function that will be called upon executing the shell command. It should have matching return type and argument types! &MyCommand