- Copy and paste the spt/features/template.cpp.txt file
- Rename to yourfeature.cpp
- Rename the default class and instance
- Code
- Auto-format your code with clang-format (you can use the script format.sh to do this automatically, can be run with Git Bash)
- Create a pull request
The Feature
base class provides some virtual functions and that you may override in your feature to do setup. There's some potential here to shoot yourself in the foot when you are trying to use things inside other features. Try to limit dependencies between features and when you have to depend on another feature, make sure:
- The feature you are depending on is going to load
- Anything you call will be ready for it. Do not rely on weird initialization order stuff, like "this feature always gets
LoadFeature
called before this one". Instead if you need something inLoadFeature
, you could solve the issue by making sure the other feature initializes that inPreHook
.
On plugin load, every feature is called to determine whether it should be loaded. Disable loading the feature in cases where you know it has no hope of working (e.g. Portal features should only load in Portal).
A function where you should init hooks and pattern finding stuff.
A function called after pattern finding stuff has completed but SPT has not yet hooked anything. This place is used to mark signals as working, add hooks in some cases, or set some state that can be read by other features. For example the HUD feature sets loadingSuccessful
depending on whether all the relevant patterns were found. This way features adding their HUD elements inside LoadFeature can be informed if the HUD element was successfully added.
This is for generic initialization code after hooking/pattern finding is done. Can:
- Add HUD elements
- Init console commands/variables
Called when the feature gets unloaded. Close any open resources like files or anything like that.
There's some macros to help finding patterns and functions in feature.hpp.
Example:
// nosleep.cpp
HOOK_FUNCTION(inputsystem, CInputSystem__SleepUntilInput);
// patterns.hpp:
namespace inputsystem
{
PATTERNS(CInputSystem__SleepUntilInput,
"5135",
"8B 44 24 ?? 85 C0 7D ??",
"5377866-BMS_Retail",
"55 8B EC 8B 45 ?? 83 CA FF");
} // namespace inputsystem
HOOK_FUNCTION
is a macro for hooking a function. This example hooks the function CInputSystem::SleepUntilInput
in inputsystem.dll. The macro assumes that you have member called ORIG_CInputSystem__SleepUntilInput
and a static function for the class called HOOKED_CInputSystem__SleepUntilInput
.
After SPT has hooked the function the HOOKED_CInputSystem__SleepUntilInput
is called instead of the original function and the original function can be called using the function pointer ORIG_CInputSystem__SleepUntilInput
.
You should hook a function whe you want to alter the behavior of the function. If you don't need to alter the behavior, use FIND_PATTERN
.
Example:
// hud.cpp
FIND_PATTERN(vguimatsurface, StartDrawing);
// patterns.hpp
...
PATTERNS(
StartDrawing,
"5135",
"55 8B EC 83 E4 C0 83 EC 38 80 ?? ?? ?? ?? ?? ?? 56 57 8B F9 75 57 8B ?? ?? ?? ?? ?? C6 ?? ?? ?? ?? ?? ?? FF ?? 8B 10 6A 00 8B C8 8B 42 20");
...
FIND_PATTERN
finds a pointer to the address where the pattern is found. This pointer is placed into ORIG_StartDrawing
in this case. This is typically used for finding a function and then the pointer can be used to call the function.
HOOK_FUNCTION
assumes that you have certain functions and members declared and defined, which can be cumbersome to write manually. To help with this, feature.hpp has the macro DECL_HOOK_THISCALL
.
Example:
// nosleep.cpp
class NoSleepFeature : public FeatureWrapper<NoSleepFeature>
{
...
private:
DECL_HOOK_THISCALL(void, CInputSystem__SleepUntilInput, int nMaxSleepTimeMS);
};
which declares the function pointer ORIG_CInputSystem__SleepUntilInput
and the corresponding static function HOOKED_CInputSystem__SleepUntilInput
that takes in one argument, int nMaxSleepTimeMS
. The fully expanded macro looks as follows:
using _CInputSystem__SleepUntilInput = void(__fastcall*)(void* thisptr, int edx, int nMaxSleepTimeMS);
_CInputSystem__SleepUntilInput ORIG_CInputSystem__SleepUntilInput = nullptr;
static void __fastcall HOOKED_CInputSystem__SleepUntilInput(void* thisptr, int edx, int nMaxSleepTimeMS);
The first line declares the type of the function pointer with the "using" statement. The second line declares the member and the last line declares the function. The first two arguments to the hooked function thisptr and edx are implicit arguments for a member function call. Thisptr is a pointer to the class instance. The edx argument seems to be unused.
DECL_HOOK_THISCALL
is only for hooking member functions, which are 95% of the functions you need to hook. DECL_HOOK_CDECL
can be used to declare hooks for normal functions. The only difference between these macros is that the calling convention is different and this macro does not declare the implicit thisptr and edx arguments.
These macros are the same as the above but they do not declare the function itself. These should be used when you only intend to call the function but do not want to change how it works.
A function declaration needs a matching definition to do anything. To help with writing the function signature, you can use the macro HOOK_THISCALL
. Example:
HOOK_THISCALL(void, NoSleepFeature, CInputSystem__SleepUntilInput, int nMaxSleepTimeMS)
{
if (y_spt_focus_nosleep.GetBool())
nMaxSleepTimeMS = 0;
spt_nosleep.ORIG_CInputSystem__SleepUntilInput(thisptr, edx, nMaxSleepTimeMS);
}
As you might have guessed, the macro HOOK_CDECL
can be used to write the function signature for normal function hooks. Example:
HOOK_CDECL(void, DemoStuff, Stop)
{
if (spt_demostuff.ORIG_Stop)
{
spt_demostuff.isAutoRecordingDemo = false;
spt_demostuff.currentAutoRecordDemoNumber = 1;
spt_demostuff.ORIG_Stop();
}
}
In some cases you have multiple patterns under the same name to support multiple versions of the game. You might also want to do some pattern specific setup, to account for differences between versions. To determine the pattern used, you can use the function GetPatternIndex. Example:
// patterns.hpp:
PATTERNS(
SpawnPlayer,
"5135",
"83 EC 14 80 3D ?? ?? ?? ?? 00 56 8B F1 74 ?? 6A 00 B9 ?? ?? ?? ?? E8 ?? ?? ?? ?? EB 23 8B 0D ?? ?? ?? ?? 8B 01 8B 96 ?? ?? ?? ?? 8B 40 0C 52 FF D0 8B 8E ?? ?? ?? ?? 51 E8 ?? ?? ?? ?? 83 C4 04 8B 46 0C 8B 56 04 8B 52 74 83 C0 01",
"5339",
"55 8B EC 83 EC 14 80 3D ?? ?? ?? ?? 00 56 8B F1 74 ?? 6A 00 B9 ?? ?? ?? ?? E8 ?? ?? ?? ?? EB 23 8B 0D ?? ?? ?? ?? 8B 01 8B 96 ?? ?? ?? ?? 8B 40 0C 52 FF D0 8B 8E ?? ?? ?? ?? 51 E8 ?? ?? ?? ?? 83 C4 04 8B 46 0C 8B 56 04 8B 52 74 40",
...
// pause.cpp
if (ORIG_SpawnPlayer)
{
int ptnNumber = GetPatternIndex((void**)&ORIG_SpawnPlayer);
switch (ptnNumber)
{
case 0:
pM_bLoadgame = (*(bool**)(ORIG_SpawnPlayer + 5));
pGameServer = (*(void**)(ORIG_SpawnPlayer + 18));
break;
...
Here the 0 index corresponds to the 5135 pattern, 1 to 5339 and so on.
Console commands and variables have to be initialized for SPT to load them. Otherwise they get automatically unloaded. Example:
// ipc-spt.cpp
void ipc::IPCFeature::LoadFeature()
{
if (FrameSignal.Works)
{
Init();
FrameSignal.Connect(Loop);
#ifndef OE
InitCommand(y_spt_ipc_ent);
InitCommand(y_spt_ipc_properties);
#endif
InitCommand(y_spt_ipc_echo);
InitCommand(y_spt_ipc_playback);
InitCommand(y_spt_ipc_gamedir);
InitConcommandBase(y_spt_ipc);
InitConcommandBase(y_spt_ipc_port);
}
}
The FrameSignal.Works
check checks if FrameSignal
has been initialized properly. If it is, then all the relevant commands are initialized with the macro InitCommand
and variables are initialized with the command InitConcommandBase
.
Check that everything the console variable/command needs has been successfully loaded. Ideally every SPT command/variable that loads works at least partially. If the command/variable does nothing OR crashes when it is used, it should not be initialized.