Docs · Engine notes
Reverse-engineering guide
The RE workflow: strings, xrefs, functions, profiles, anchors, offline checks.
Copied from docs/reverse-engineering.md in the V3X repository, file dated 2026-11-13.
Victoria 3 ships without symbols. Everything in docs/re/ was found with the tools below, from
strings, RTTI, .pdata and live profiling. This guide shows the workflow and every tool.
Setup: pip install pefile capstone numpy (pefile for all tools; capstone for the
disassembling ones; numpy for a few re_* scanners). Run the tools without python -I
(isolated mode hides user site-packages). Paths below are from v3x/; EXE is
../binaries/victoria3.exe.
The workflow
- Find a string near what you want (a log message, an assert, a file name, a console
command):
pestrings.py. - Find the code that uses it:
pexref.py --str(orre_sim_lib.py str), then the enclosing function from.pdata. - Read the function:
pexref.py --dis,re_sim_lib.py dis/fn, or live with the devconsole commandsfn,dis,xref(also through the MCP bridge). - Find out what is hot: devtools sampling profiler ->
profile_latest.csv->symbolize.pynames the functions by the strings they reference. - Find classes and virtual calls:
rtti_dump.py(vtables by class name, slot targets, ABI hints). - Count arguments before hooking:
re_simopt_argcheck.py. - Write the anchor (string + shape) in your plugin and an offline check that runs it
against the exe (
sdk/v3x_testkit.h, or atools/re_<topic>_check.py). - Write it down in
docs/re/<topic>.md: RVAs with the anchor used to find each one, and tags [V] verified / [I] inferred / [D] from dumps / [R] measured at runtime.
RVAs are relative to the image base (0x140000000 in the file). At runtime add
api->image_base(). Every RVA in docs/re/ is for 1.15.1 (PE TimeDateStamp 0x6af3094f);
re-anchor after a patch.
Static tools
tools/pestrings.py - strings with RVAs
python tools/pestrings.py $EXE 6 > strings.txt # minimum length 6
grep -n "Processing Tick" strings.txt
tools/pexref.py - code references to a string or an RVA
python tools/pexref.py $EXE --str "Processing Tick: "
python tools/pexref.py $EXE --rva 0x475efc0 --dis 8 20 # disassemble 8 before / 20 after each xref
Finds RIP-relative lea/mov like api->find_xref. tools/re_sim_anyref.py and
tools/re_hist_anyref.py find any RIP-relative operand (cmp, vmov, byte flags).
tools/peptr.py - absolute pointers to an RVA or string
python tools/peptr.py $EXE --str "CTickTask_UpdateAI" # vtables, tables, registries in .rdata/.data
tools/re_sim_lib.py - indexed disassembly (and its per-topic wrappers)
Builds a one-time xref index (cached as tools/re_sim_index.pkl; set RE_SIM_CACHE to put it
elsewhere).
python tools/re_sim_lib.py $EXE str "Calling global random function without having called ALLOW_RANDOM_IN_SCOPE in this thread"
python tools/re_sim_lib.py $EXE dis 3787300 40 # 40 instructions from an RVA
python tools/re_sim_lib.py $EXE fn 3787300 # start/end of the .pdata function
python tools/re_sim_lib.py $EXE calls 3787300 # direct call targets inside it
python tools/re_sim_lib.py $EXE callers 3787300 # direct callers
re_enh_lib.py, re_hist_lib.py, re_mem_lib.py, re_stream_lib.py, re_vfs_lib.py,
re_load_*.py, re_console_*.py, re_threads_*.py, re_tickpar_*.py, re_simopt_*.py are
topic helpers written by the workers of each area; each has a usage docstring at the top.
tools/rtti_dump.py - MSVC RTTI: classes, vtables, slots
python tools/rtti_dump.py $EXE --prefix CTickTask_ --slots 8 --slot 6
Same algorithm as include/v3x_rtti.h (which your plugin uses at runtime). The slot analysis
prints target size, sharing between classes, first instructions and ABI hints (xmm reads, stack
arg reads, float returns).
tools/re_simopt_argcheck.py - how many arguments does it take?
python tools/re_simopt_argcheck.py $EXE e02c90 146d010
Two independent signals: what callers write to [rsp+0x20..] before the call, and what the
callee reads above its home slots. If either shows stack arguments, a 4-argument detour is wrong.
(hook_safe makes this moot for up to 16 arguments.)
tools/re_mem_catch.py - who catches which C++ exception
python tools/re_mem_catch.py $EXE ".?AVbad_alloc@std@@"
tools/dmpinfo.py, tools/re_mem_dmp.py - minidumps
python tools/dmpinfo.py <crash.dmp> # exception, faulting module+offset, C++ type, exe return addresses
python tools/re_mem_dmp.py <crash.dmp> # memory summary
The game’s crash reporter writes dumps under Documents/Paradox Interactive/Victoria 3/crashes/.
Runtime tools (plugins)
devconsole - read-only RE commands in the running game
Type into the V3X console (help lists them):
fn +0x1465690 whole function containing the RVA, Zydis disassembly
dis victoria3.exe+0x1465690 30
xref "Processing Tick: " rip-relative references to an exact string (or an address)
scan 48 8B 05 ?? ?? ?? ?? 48 85 C0
mem <addr> 256 hex + ASCII; unreadable memory prints an error, never crashes
mods loaded modules
Addresses: hex, +rva (in the exe), or module+offset.
devtools - sampling profiler
binaries/v3x/devtools.ini: profile=1 (or env V3X_PROFILE=1, or console profile on).
About 1 kHz on the main thread + the 4 busiest threads; unwind_depth=8 frames for inclusive
time. Outputs in binaries/v3x/: devtools.log (CPU per thread, memory, GPU memory every
interval_sec), profile_latest.csv (rva, self samples, inclusive, module),
profile_edges.csv (caller -> callee), and per load transition
profile_<from>_to_<to>_<n>.csv.
python tools/symbolize.py --exe $EXE --top 50 ../binaries/v3x/profile_latest.csv
python tools/re_simopt_tree.py profile.csv profile_edges.csv <rva> # hot call tree below a function
python tools/re_map_chain.py profile_edges.csv <rva> --exe $EXE # who calls it
python tools/re_load_attrib.py profile_latest.csv # attribution for load phases
symbolize.py names functions by the strings they reference (source paths first) and, for
functions without strings, by their callees.
tick_profiler - time per tick task
Hooks RunTask; console ticks; binaries/v3x/tick_profile.csv every 60 s.
alloc_tracker - allocations by call site
binaries/v3x/alloc_tracker.ini: enabled=1. Hooks mimalloc mi_new/mi_malloc/mi_free;
console allocs [top N]; binaries/v3x/alloc_report.txt (live and total bytes per call site).
Use it to find who holds memory; turn it off again afterwards (it costs a few ns per allocation).
crash_logger - what happened
binaries/v3x/crash.log: C++ throws with type names, fatal exceptions with registers and stack
as module+offset. See troubleshooting.md.
devbridge + MCP - drive the console from a tool or an AI assistant
plugins/devbridge serves the V3X console over the named pipe \\.\pipe\v3x_devbridge
(env V3X_DEVBRIDGE_PIPE changes the name). mcp/v3x_mcp.py is a stdio MCP server (Python
standard library only) that exposes it as tools: v3x_exec (any console line), v3x_dis,
v3x_fn, v3x_xref, v3x_mem, v3x_profile_report, v3x_ticks. Register it from
.mcp.json.example (replace the path with yours). Every call opens the pipe, runs one command,
returns its captured output (exec_capture) and closes it.
Engine-specific tips
- Assert and log strings are the best anchors. The engine keeps source file names
(
gamestatetick.cpp:1253), format strings ("Tick Tasks for {}") and assert texts. - Console commands are registered by name;
tools/re_console_cmds.pylists their descriptors and handlers (docs/re/console.md). A console command that does what you want is a short path to the function that does it. - Tick tasks are classes
CTickTask_<Name>: find the vtable by RTTI, slot 6 is Execute (docs/re/simulation.md). - Split functions: MSVC moves cold code into separate chunks with chained unwind info.
function_startfollows the chain; your shape checks must use the primary function. - Leaf functions (e.g.
mi_free) have no.pdataentry:function_startreturns NULL; find them by their callers (tools/re_mem_count.py). - Statically linked libraries (mimalloc 2.0.x, spdlog, PhysFS 3, SDL, Zstd…) can be matched against their open-source code: compare strings and constants.
Writing it up
Each docs/re/<topic>.md starts with the binary it was made on, the tools used, and the tags.
For every RVA give the anchor that finds it (string, pattern, caller) so the next person can
re-find it after a patch. Put “Not verified” in its own section.