Console variable and command recovery plugin for IDA Pro 9.x with complete cross-platform support.
Finds every ConVar and ConCommand a Source 2 binary registers, names the globals after them, decodes their flags, and lists every place the engine touches them.
I build free, open-source tools for the community. If they save you time, you can support the work through GitHub Sponsors. One-time or monthly, any amount, no tiers and nothing locked behind it.
⭐ Or support me for free by starring this repository!
Core Capabilities
- Registration constructor discovery, nothing hardcoded to a game build
- ConVar and ConCommand separation from how the objects are used
- Automatic renaming of every registered global (
cvar_sv_cheats,ccmd_bot_add) - Command handler recovery, the function behind a command gets named
cmd_bot_add - The constructors and the value accessor get their hl2sdk names, but only where the argument shape proves which one they are
- Descriptions written back as repeatable comments
- FCVAR flag names read out of the binary, with an hl2sdk reference table as backup
- Use site listing: every place a variable's object is referenced, and what each site calls
- Prefix learning, the seeds find one constructor and its call sites teach the rest
- Engine gate, the scan only runs on binaries that actually look like Source 2
Platform Support
- IDA Pro 9.x (modern SDK)
- Windows x64
- Linux x64/ARM64
- macOS Intel/Apple Silicon
Integration
- Native IDA chooser interface
- Nested
Source2Forgemenu with separate ConVar and Command views - Rename offer on first analysis, no scan is started before autoanalysis finishes
- JSON and CSV export
Download binary for your platform from releases
Copy to IDA plugins directory:
Windows: C:\Program Files\IDA Pro 9.x\plugins\source2forge64-windows-x64.dll
Linux: /opt/ida-pro/plugins/source2forge64-linux-{x64,arm64}.so
macOS: /Applications/IDA Pro 9.x.app/Contents/MacOS/plugins/source2forge64-macos-{arm64,x64}.dylib
Restart IDA Pro
Open a Source 2 binary (server.dll, client.dll, engine2.dll, or their Linux counterparts). When autoanalysis finishes the plugin offers to name every ConVar object it found. Accept and the globals stop being qword_25ABF00.
Menu
Right-click anywhere in the disassembly or pseudocode view:
Source2Forge
ConVars → List
→ Export
Commands → List
→ Export
Name all ConVar objects
In the list
Type to filter by name, flag or description. Enter opens the use sites of the selected entry.
Rows carrying FCVAR_CHEAT are tinted red. Rows that are never touched outside their registration are greyed out, those are the variables the binary registers but never reads.
Every ConVar with its object address, use count, decoded flags and description
The same for commands, cheat protected ones tinted red
Use sites
Selecting a variable and pressing Enter opens every place its object is referenced, with the function it sits in and the accessor that consumes it. Source 2 hands back a pointer to the value and lets the caller decide what to do with it, so the sites are listed rather than guessed at.
After naming
The handler, the objects and their descriptions, all recovered from the registration
Server plugin development
// Source2Forge shows: mp_friendlyfire | FCVAR_NOTIFY | FCVAR_REPLICATED | 0x181FDF398
auto mp_friendlyfire = (ConVar*)(base + 0x1FDF398); // address straight from the listReverse engineering
- Find which convars a build added, removed or re-flagged between versions via the JSON export
- Locate the accessor a game uses to read values, then work backwards from there
- Spot cheat-protected and replicated variables before touching them
- Recover command names for a binary that ships without symbols
Documentation
- Export the full convar table with flags and descriptions to CSV for a wiki
- Diff two exports to build a changelog for a game update
Constructor discovery
Every ConVar registration passes the variable name as a string. The plugin walks the string list, keeps the ones shaped like a convar name, and follows each reference to the first call after it. Whatever function collects the most of those calls is a registration constructor candidate.
Argument profiling
A candidate's call sites are then read back to learn what each argument slot carries:
0x1815172F0 rcx = object rdx = name r8 = flags r9 = description
0x1815187F0 rcx = object rdx = name r8 = callback r9 = description [rsp+20h] = flags
0x180182B50 rcx = object rdx = name r8 = type r9 = flags [rsp+20h] = description
Nothing about this is tied to a game build. Addresses move every patch, the shape does not. MS x64 spills the fifth argument onto the stack, which is why command flags are read from [rsp+20h] and not from a register.
Validation
A string helper takes a this pointer and a char* too, so shape alone is not enough. What separates them is that a helper pours many different names into one buffer while a registration gives every object exactly one name. Counting distinct objects instead would punish a reference looked up from several places, which is normal. Entity factories survive that test as well, so they are separated by whether their objects are ever handed to a value accessor.
Variable or command
A variable has to be read by something, so its object shows up again after registration. A command object is written once and never touched again, the dispatch goes through the callback it was given. Measured on a cs2 server.dll the split is 0 of 356 against 86-100%, so the vote is taken per constructor and never per object.
Command handlers
A ConCommand's callback never reaches the constructor directly. It is loaded with a lea, dropped into a stack delegate, and only a pointer to that delegate is passed. The plugin reads the load back instead:
lea rax, sub_1801BBEE0 ; the handler
mov [rsp+58h+var_28], rax
lea r9, aLoadsAFileCont ; description
lea r8, [rsp+58h+var_28] ; only points at the delegate
lea rdx, aSvLoadForcedCl ; name
lea rcx, unk_181DA9C08 ; the object
mov [rsp+58h+var_38], 80004h ; flags, the 5th argument
call sub_1815187F0Two different functions in the window means it cannot be told which one it is, and those are skipped rather than guessed. On a cs2 server.dll that recovers 339 of 356 handlers.
Engine functions
CConVar's constructor forwards to ConVarRefAbstract::Register( name, flags, help_string, value_info ), and both take the same registers, so the argument shape cannot tell them apart. What can is the call graph: the per type wrappers call the core and the core calls none of them. The core gets the name, the wrappers keep their address name because there is no way to know which T each was built for.
Commands are easier. A name, a help string and a callback struct is a shape nothing else passes, so that one is named outright. The accessor most variables are read through becomes ConVarData::Value.
The typed getters sit on top of that accessor. ConVarRefAbstract::GetFloat and friends are tiny wrappers whose only call is Value(slot), and the load out of the returned pointer says which type each was built for: movss is a float, a byte load is a bool, a dword an int, a qword a string. A game helper written around one convar looks identical from the outside, so only the wrapper the whole binary shares is named, and a close runner up means neither is.
UnRegisterConVar is named the same way. Every convar's destructor is a stub that hands its object over and does nothing else, so the function all those stubs call is the free void UnRegisterConVar( ConVarRef *cvar ).
The compiler emits these small functions once per translation unit, so the binary carries several copies of each at different addresses. A copy is not byte equal because every branch inside it is relative, but it has the same opcodes and every reference that differs is a jump that moved with the function. Copies get the name their original got, and IDA hangs a _0 on them.
Nothing else is named. A real SDK name sitting on the wrong function is worse than sub_180182B50, and ConVar_Register in particular is a genuine tier1 function that has nothing to do with any constructor.
Flags or value type
A constructor that takes a name but no description is a reference to a variable defined elsewhere, and its small integer argument is the value type, not the flag word. Those are listed without flags rather than with wrong ones.
Prefix learning
The seed list only has to find one constructor. Its call sites are then scanned for the prefixes this binary actually uses, and those find the constructors the seeds never covered. Two or three rounds and it stops growing.
Three IDC functions expose the same data the lists show, callable from IDAPython:
import idc, json
convars = json.loads(idc.eval_idc('Source2Forge_Scan()'))['convars']
cheats = [c for c in convars if 'FCVAR_CHEAT' in c['flags_text']]
print(len(cheats), 'cheat protected')
one = json.loads(idc.eval_idc('Source2Forge_Uses(0x181FDF398)'))
print(one['convars'][0]['uses'])
print(idc.eval_idc('Source2Forge_Rename()'), 'objects named')| Function | Returns |
|---|---|
Source2Forge_Scan() |
Every entry as JSON, scanning first if needed |
Source2Forge_Uses(ea) |
The single entry registered at that object address |
Source2Forge_Rename() |
Number of objects it named |
Source2Forge_Rescan() |
Drops the cache and scans again, logging every constructor candidate and why it was taken or dropped. Run this when a binary comes back with fewer entries than expected |
tests/test_idc_export.py runs against these. Open a Source 2 binary, let the analysis finish, then File → Script file... and pick it. Every check is about the shape of the data rather than a specific build, so it stays valid across game patches.
Prerequisites
- IDA SDK extracted to
sdk/directory - Docker (for Linux/macOS builds)
- LLVM/Clang and xwin (for the Windows cross-compile)
Build Commands
# Linux + macOS (via Docker)
make build
# Windows x64 (cross-compile with clang-cl + lld-link)
make build-windows
# everything, all five binaries
make build-allNative Windows builds go through CMake instead:
cmake -B build -DIDASDK_DIR=C:/idasdk90
cmake --build build --config ReleaseMIT, see LICENSE.

