Memory and Modules
Find functions and vtables in game binaries — pattern scanning, vtables by name, offset or base class, and address arithmetic.
When no Source2Mod interface or ADDR_* getter covers what you need, you go to
the binaries yourself. ISource2ModModule wraps a loaded library (server,
engine2, tier0 ...) for pattern scans, exports and vtables;
ISource2ModMemory is the address value type those calls return.
#include "source2mod/ISource2ModModule.h" // pulls in ISource2ModMemory.hBefore scanning by hand, check whether the address already exists: the
Source2Mod resolves many engine functions as ADDR_*() getters
(ISource2ModAddresses.h), and any gamedata entry can be resolved with
GAMECONFIG_RESOLVE("Name"). Both survive game updates through the gamedata
files instead of patterns compiled into your plugin.
Getting a module
| Way | Release with |
|---|---|
ISource2ModModule::New("server") | delete |
ISource2ModModule::New(ptr) — any address inside the module (uintptr_t, void* or ISource2ModMemory) | delete |
LOAD_MODULE(name) → g_Source2ModAPI->LoadModule(name) | FREE_MODULE(mod) |
LOAD_MODULE_MEM(ptr) → g_Source2ModAPI->LoadModuleFromMemory(ptr) | FREE_MODULE(mod) |
Names are given without extension. New(name) is the same as
LoadModule(name); delete and FreeModule() are interchangeable.
// From an interface pointer you already have: whatever module it lives in.
ISource2ModModule* server = ISource2ModModule::New(static_cast<void*>(g_Source2ModAPI->GetSource2Server()));
g_Source2ModAPI->ConPrintf("server module: %s\n", server->GetModulePath());Keep module objects for as long as you use them (a global created in Load()
and deleted in Unload() is typical) — they are cheap to keep and the strings
they return (GetModulePath(), GetModuleName()) live as long as the object.
Pattern scanning
ISource2ModMemory FindPattern(const char* pattern, ISource2ModMemory startAddress = ISource2ModMemory()) const;
ISource2ModMemory FindPatternInSection(const char* pattern, const char* section, ISource2ModMemory startAddress = ISource2ModMemory()) const;Patterns are IDA-style strings: space-separated hex bytes, ?? as a
wildcard. FindPattern scans the executable section; FindPatternInSection
takes a section name such as ".text" or ".rdata". A miss returns a null
ISource2ModMemory — always check it.
ISource2ModMemory addr = server->FindPattern("48 89 5C 24 ?? 57 48 83 EC ?? 48 8B D9 E8");
if (!addr)
{
g_Source2ModAPI->ConPrintf("MyFunction not found -- game update?\n");
return false;
}
using MyFunction_t = void (*)(CBaseEntity*);
auto pfn = addr.RCast<MyFunction_t>();A pattern differs between the Windows and the Linux binary. WIN_LINUX(win, lin)
(ISource2ModModule.h) picks the platform's one of the two at compile time — for
a pattern, an offset or a library name:
ISource2ModMemory addr = server->FindPattern(WIN_LINUX("48 89 7C 24 ? 55", "55 48 89 E5 41 57"));Prefer a gamedata entry for anything the game moves; that is updated without a
rebuild. WIN_LINUX is for a pattern a plugin keeps to itself.
Following references
Patterns often land on an instruction that references what you want rather
than on it. ISource2ModMemory resolves the RIP-relative operand:
// E8 xx xx xx xx -- a near call: follow it to the callee.
ISource2ModMemory callee = addr.FollowNearCall(); // opcode offset 1, instruction length 5
// 48 8B 05 xx xx xx xx -- mov rax, [rip+disp32]: operand at +3, instruction length 7.
ISource2ModMemory global = addr.ResolveRelativeAddress(0x3, 0x7);Symbols and vtables
| Call | Returns |
|---|---|
GetFunctionByName(name) | An exported symbol |
GetVirtualTableByName(name, decorated = false) | A class's primary vtable |
GetVirtualTableByBase(name, baseName, decorated = false) | The vtable of a (non-virtual) base-class sub-object, found through RTTI |
GetVirtualTableByOffset(name, offset, decorated = false) | The vtable of the sub-object at a byte offset |
Class names are undecorated unless you pass decorated = true. A class with
several bases has one vtable per base not at offset 0;
GetVirtualTableByName only gives the primary one. For an interface that is
not the first base, prefer GetVirtualTableByBase — the offset moves whenever
the class is laid out anew, the base name does not.
ISource2ModMemory vtable = server->GetVirtualTableByName("CCSPlayerPawn");
// "CSomeClass" / "ISomeInterface": a class and one of its non-first bases.
ISource2ModMemory iface = server->GetVirtualTableByBase("CSomeClass", "ISomeInterface");The KHOOK_VTABLE(module, name) and KHOOK_VTABLE_BASE(module, name, base)
hook targets resolve their tables through these same calls — see
Hooks.
GetVirtualTableByOffset and GetVirtualTableByBase were appended to the
interface later. A plugin calling them needs a s2m core that has them.
Module inspection
GetSectionByName(name) returns a SectionInfo { base, size } (check
IsValid()), GetModuleBase() the load address, GetModuleHandle() the OS
handle, GetModulePath() / GetModuleName() the file.
ISource2ModMemory
A thin value type over an address — plugins use it instead of DynLibUtils directly.
| Member | Does |
|---|---|
operator bool, ==, != | Null check and comparison |
GetPtr(), GetAddr() | As void* / uintptr_t |
RCast<T>(), CCast<T>(), UCast<T>() | Cast to a pointer or function type |
GetValue<T>() | Read a T at the address |
Offset(n), OffsetSelf(n) | Add a byte offset |
Deref(n = 1), DerefSelf(n = 1) | Follow pointers n times (stops at null) |
FollowNearCall(opcodeOffset = 0x1, nextInstructionOffset = 0x5) | Target of a relative call |
ResolveRelativeAddress(registerOffset = 0x0, nextInstructionOffset = 0x4) | Target of any RIP-relative operand |
The *Self variants modify the object in place and return it.
Pitfalls
- Free what you create. Every
New()/LOAD_MODULE()result is a heap object; release it withdeleteorFREE_MODULE(). Source2Mod does not track modules per plugin, so one you keep pastUnload()leaks. - Patterns break on game updates. Keep them in gamedata, not in code, when you can; always handle a null result instead of calling through it.
- Windows macro clashes.
windows.hdefinesGetModuleHandleandFreeModuleas macros; the SDK headers#undefthem. Include the SDK headers afterwindows.h, or the method names get rewritten. - Platform differences. Patterns, vtable layouts and decorated names differ
between Linux and Windows builds — guard platform-specific data with
#ifdef _WIN32.