Guides

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.

plugin.cpp
#include "source2mod/ISource2ModModule.h" // pulls in ISource2ModMemory.h

Before 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

WayRelease 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.

plugin.cpp
// 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

CallReturns
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.

MemberDoes
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 with delete or FREE_MODULE(). Source2Mod does not track modules per plugin, so one you keep past Unload() 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.h defines GetModuleHandle and FreeModule as macros; the SDK headers #undef them. Include the SDK headers after windows.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.

Reference

On this page