V3Xengine works for Victoria 3

Unofficial · not affiliated with Paradox Interactive

Docs · For modders

Building and testing

Build, repository layout, the offline SDK testkit and end-to-end tests.

Copied from docs/testing.md in the V3X repository, file dated 2026-11-13.

Build

pip install ziglang                 # the compiler (no Visual Studio needed)
./build.sh                          # build/version.dll + build/plugins/*.dll
./build.sh install                  # ... and copy them into ../binaries (the game)
python sdk/build_plugin.py plugins/<name> [--test] [--install]   # one plugin
python installer/make_dist.py       # build + build/dist/V3X-Setup.exe (player installer, docs/install.md)

Repository layout

include/v3x.h        public plugin API (C ABI, versioned, append-only)
include/v3x_rtti.h   MSVC RTTI lookup helpers for plugins
sdk/*.h              header-only helpers: v3x_sdk.h (all), mem, scan, anchor, ini, hook, game, util, script, testkit
sdk/build_plugin.py  one-command build / offline test / install / check of a plugin
templates/           plugin_template: minimal plugin + ini + offline test
src/proxy.c          version.dll export forwarding
src/core.c           boot, plugin loader, scanning, hooks, frame tick, console, events, manifests
src/safe_hook.c      the pass-through thunk behind api->hook_safe (API v4)
plugins/*            the bundled plugins (docs/plugins.md)
installer/           player installer V3X-Setup.exe (setup.c, make_dist.py, profile.txt, test_installer.py)
test/*.c, test/*.py  tests without the game (this page)
tools/*.py           reverse-engineering helpers (docs/reverse-engineering.md)
mcp/v3x_mcp.py       MCP server for the devbridge pipe
docs/*.md            documentation (docs/README.md); docs/re/*.md reverse-engineering notes
third_party/minhook  inline hooking (BSD-2), git submodule
third_party/zydis    x86 disassembler, amalgamated (MIT); used by devconsole

Test runner

There is no CI. A single runner test/run_all.sh is being written by the sdk worker (2026-11-13; the file does not exist yet at the time of writing). test/api_v5_test.c, test/compat_test.c (mode old) and test/compat_check.py say in their headers that run_all.sh builds their setup. Until it lands, run each test by hand as below.

Setup (once per shell)

Pick a scratch directory with free space (S); the compiler’s temp and cache directories can go there too (several GB over time). From v3x/:

S=${V3X_TMP:-$TEMP/v3x}                      # e.g. V3X_TMP=D:/v3x_tmp when C: is short on space
export TEMP=$S TMP=$S ZIG_GLOBAL_CACHE_DIR=$S/zcache ZIG_LOCAL_CACHE_DIR=$S/zcache
CC="python -m ziglang cc -target x86_64-windows-gnu -O2 -Iinclude"
M=third_party/minhook; MH="$M/src/buffer.c $M/src/hook.c $M/src/trampoline.c $M/src/hde/hde64.c"
EXE=../binaries/victoria3.exe; T=$S/t; mkdir -p $T
./build.sh                                   # build/version.dll + build/plugins/*.dll

Some older tests write fixed scratch folders under D:/v3x_tmp/ (alloc_tracker, console_plus, fast_load, lazy_gfx, lazy_mesh, map_cache, mod_loader: see their header comments); they need a D: drive until they are converted.

Every test prints one line per check and ALL OK at the end (exit code 0). Arguments in [ ] are optional; with $EXE a test maps the real exe as an image (no game code runs) and checks the plugin’s anchors against it. The build line of each test is copied from its header comment (test/<file>, line 1 on), which has the full details.

Every test

test file covers kind
test/abi_test.c ABI freeze of include/v3x.h (struct layouts, offsets, constants) compile-time + unit
test/hook_safe_test.c src/safe_hook.c: 0-16 args, floats, struct return, recursion, threads, unwind, registers unit
test/hook_chain_test.c several hook_safe subscribers on one target, exclusive raw hooks, per-owner unhook unit
test/sdk_test.c sdk/ helpers (ini, mem, scan, anchor, hook, manifest, util); script-RNG anchor with $EXE unit
test/rtti_test.c include/v3x_rtti.h on fake RTTI; with $EXE, the CTickTask_ count unit
test/harness.c loader end to end + ironman_console e2e
test/api_v4_test.c + test/v4_plugin.c API v4 via the loader: hook_safe, ids, enum_commands, manifests, [plugins] skip list e2e
test/api_v5_test.c + test/api_v5_plugin.c API v5 + loader hardening: log levels, reload, fault isolation, rollback, crash-loop marker, build gate e2e (in progress, sdk worker)
test/compat_test.c + test/compat_plugin.c + test/compat_v1..v4.h plugins built for API v1-v5 load on the current loader; v5 plugin degrades on a v4 loader e2e
test/compat_check.py sdk/build_plugin.py check (DLL validation without loading) python
test/shutdown_test.c + test/shutdown_probe.c every plugin lets the process exit within 10 s, 20 runs x 2 modes e2e
test/crash_test.c crash_logger: C++ throws, AV, abort, stack overflow e2e
test/devconsole_test.c devconsole commands, fault-proof reads, exec_capture e2e
test/devbridge_test.c devbridge pipe protocol e2e
test/test_mcp.py mcp/v3x_mcp.py against devbridge_test.exe --serve N python
test/devtools_test.c devtools thread / profiler / memory reports, per-phase CSVs e2e
test/ai_par_test.c ai_par logic, RRP hook + lever, anchors on $EXE offline
test/alloc_tracker_test.c alloc_tracker tables, sampler; built DLL against $EXE offline
test/asset_stream_test.c asset_stream config, anchors, detour end to end offline
test/console_plus_test.c console_plus completion logic, fake registry, anchors, console-window Tab offline
test/content_packs_test.c content_packs decoders, scan / order / mount logic offline
test/cpu_sched_test.c cpu_sched P/E classification, anchors, built DLL detours offline
test/enhance_test.c enhance anchors, patches, speed, save events, withdrawn fast_speed5 offline
test/fast_load_test.c fast_load fl_core + mesh thunk stub (MinHook) offline
test/fast_lookup_test.c fast_lookup caches vs the real engine code on $EXE, hooked = unhooked offline
test/frame_pacer_test.c frame_pacer config, policy, anchors offline
test/gfx_driver_test.c gfx_driver ini, retry path, IAT slot in $EXE offline
test/keep_history_test.c keep_history: 11 patch sites + 2 record_all hooks on $EXE offline (needs $EXE)
test/lazy_gfx_test.c lazy_gfx stages, manifest, prefetch, png_cache, 7 hook_safe calls on $EXE offline
test/lazy_mesh_test.c lazy_mesh table, lm_ensure, hooks against fake engine, 6 anchors offline
test/loc_fallback_test.c loc_fallback parser, missing-key computation, file generation offline
test/log_filter_test.c log_filter rule engine offline
test/map_cache_test.c map_cache pin table, detours vs a fake weak texture cache, 3 anchors offline
test/mem_tuner_test.c mem_tuner option table walk, patterns, built DLL against $EXE offline
test/mod_loader_test.c mod_loader index, predictions, mode=on skips, verify sampling offline
test/mod_menu_test.c mod_menu ini rewrite, GUI generation, registrar detour, anchors offline
test/par_load_test.c par_load thunks, db_loop patching, ordered fork/join determinism, anchors offline
test/script_ext_test.c script_ext pack writing, BOM, manifest cleanup, RE anchors of docs/re/script.md offline
test/sim_opt_test.c sim_opt anchors, diag, throttle, diag_trades / diag_journal, 6-arg pass-through offline
test/tick_par_test.c tick_par scheduling logic, stub + call-site patch, mode=on refused offline
test/tick_profiler_test.c tick_profiler detour, report, CSV, process exit offline
installer/test_installer.py V3X-Setup.exe end to end on fake game folders python
templates/plugin_template/test_my_plugin.c the template plugin (via build_plugin.py --test) offline

hello has no test of its own (test/api_v4_test.c loads hello.dll to test the skip list).

Core, API and SDK

$CC -o $T/abi.exe test/abi_test.c && $T/abi.exe
$CC -I$M/include -o $T/hst.exe test/hook_safe_test.c $MH && $T/hst.exe
$CC -I$M/include -o $T/hct.exe test/hook_chain_test.c $MH && $T/hct.exe
$CC -Isdk -Wno-unused-function -o $T/sdkt.exe test/sdk_test.c && $T/sdkt.exe [$EXE]
$CC -o $T/rtti.exe test/rtti_test.c && $T/rtti.exe [$EXE <expected CTickTask_ count>]
python sdk/build_plugin.py templates/plugin_template --out $T/tpl --test [$EXE]

Plugin offline tests

They #include the plugin source or load the built DLL against a fake V3X_API.

$CC -Isdk -o $T/aipar.exe test/ai_par_test.c && $T/aipar.exe [$EXE $T/]
$CC -o $T/att.exe test/alloc_tracker_test.c && $T/att.exe [$EXE build/plugins/alloc_tracker.dll]
$CC -o $T/ast.exe test/asset_stream_test.c && $T/ast.exe plugins/asset_stream/asset_stream.ini.example [$EXE [$T]]
$CC -o $T/cplt.exe test/console_plus_test.c && $T/cplt.exe [$EXE]
mkdir -p $T/cp && $CC -o $T/cpt.exe test/content_packs_test.c && $T/cpt.exe $T/cp
$CC -o $T/cst.exe test/cpu_sched_test.c && $T/cst.exe [$EXE|- [build/plugins/cpu_sched.dll]]
mkdir -p $T/enh && $CC -o $T/enht.exe test/enhance_test.c && $T/enht.exe $T/enh [$EXE]
$CC -I$M/include -o $T/flt.exe test/fast_load_test.c $MH && $T/flt.exe [$EXE]
$CC -I$M/include -o $T/fkt.exe test/fast_lookup_test.c $MH && $T/fkt.exe [$EXE [$T]]
$CC -o $T/fpt.exe test/frame_pacer_test.c && $T/fpt.exe plugins/frame_pacer/frame_pacer.ini.example [$EXE]
$CC -o $T/gdt.exe test/gfx_driver_test.c && $T/gdt.exe plugins/gfx_driver/gfx_driver.ini.example [$EXE]
$CC -o $T/kht.exe test/keep_history_test.c && $T/kht.exe $EXE build/plugins/keep_history.dll
$CC -Isdk -o $T/lgt.exe test/lazy_gfx_test.c && $T/lgt.exe [$EXE]           # writes D:/v3x_tmp/lazygfx/t*
$CC -o $T/lmt.exe test/lazy_mesh_test.c && $T/lmt.exe [$EXE]                # writes D:/v3x_tmp/lazymesh/t/
mkdir -p $T/lf && $CC -o $T/lft2.exe test/loc_fallback_test.c && $T/lft2.exe $T/lf [<game root> <docs dir> [<lang> [build/plugins/loc_fallback.dll]]]
$CC -o $T/lft.exe test/log_filter_test.c && $T/lft.exe plugins/log_filter/log_filter.ini.example
$CC -DMC_TEST -o $T/mct.exe test/map_cache_test.c && $T/mct.exe [$EXE]      # writes D:/v3x_tmp/mapmode/t/
$CC -o $T/mtt.exe test/mem_tuner_test.c && $T/mtt.exe [$EXE [build/plugins/mem_tuner.dll [api_version]]]
$CC -o $T/mlt.exe test/mod_loader_test.c && $T/mlt.exe [$EXE]               # writes D:/v3x_tmp/modload/t
mkdir -p $T/mm && $CC -o $T/mmt.exe test/mod_menu_test.c && $T/mmt.exe $T/mm [../game/gui/frontend/frontend_main.gui [$EXE]]
$CC -Isdk -o $T/plt.exe test/par_load_test.c && $T/plt.exe [$EXE]
mkdir -p $T/sx && $CC -Isdk -o $T/sxt.exe test/script_ext_test.c && $T/sxt.exe $T/sx [$EXE]
$CC -o $T/sot.exe test/sim_opt_test.c && $T/sot.exe
$CC -o $T/tpar.exe test/tick_par_test.c && $T/tpar.exe
$CC -o $T/tpt.exe test/tick_profiler_test.c && $T/tpt.exe

Notes: the header of loc_fallback_test.c has only the usage line; the build line above was checked on 2026-11-13 (ALL OK without the optional arguments). loc_fallback_test.c, mod_menu_test.c and script_ext_test.c want an empty temp dir.

End-to-end tests

They link build/version.dll like victoria3.exe does, so V3X boots and loads <dir>/v3x/plugins/*.dll before main. e2e <test> <plugins...> builds one into $T/<test>/ with only the plugins it needs:

e2e() { D=$T/$1; mkdir -p $D/v3x/plugins && cp build/version.dll $D/ && $CC -o $D/$1.exe test/$1.c -Lbuild -l:version.dll -luser32 && shift && for p in "$@"; do cp build/plugins/$p.dll $D/v3x/plugins/; done; }
e2e harness ironman_console && (cd $D && V3X_NO_CONSOLE=1 ./harness.exe)                 # loader + ironman_console
e2e devconsole_test devconsole && (cd $D && V3X_NO_CONSOLE=1 ./devconsole_test.exe)      # commands, fault-proof reads, exec_capture
e2e devbridge_test devconsole devbridge && (cd $D && V3X_NO_CONSOLE=1 V3X_DEVBRIDGE_PIPE=v3x_devbridge_test ./devbridge_test.exe)  # private pipe required
python test/test_mcp.py $T/devbridge_test/devbridge_test.exe                               # MCP server against devbridge_test --serve
e2e devtools_test devtools && echo interval_sec=2 > $D/v3x/devtools.ini && (cd $D && V3X_NO_CONSOLE=1 V3X_PROFILE=1 ./devtools_test.exe)
e2e crash_test crash_logger && (cd $D && V3X_NO_CONSOLE=1 ./crash_test.exe)               # runs copies of itself in child_<mode>/

API v4 (hook_safe through the loader, main_thread_id, game_build_id, enum_commands, manifests incl. min_api / REQUIRE_BUILD refusals, the [plugins] skip list), from test/api_v4_test.c:

D=$T/api_v4_test; mkdir -p $D/v3x/plugins && cp build/version.dll $D/
$CC -shared -o $D/v3x/plugins/v4_ok.dll test/v4_plugin.c
$CC -shared -DV4_REQUIRE -o $D/v3x/plugins/v4_require.dll test/v4_plugin.c
$CC -shared -DV4_MINAPI=99 -o $D/v3x/plugins/v4_minapi.dll test/v4_plugin.c
$CC -shared -DV4_WARN -o $D/v3x/plugins/v4_warn.dll test/v4_plugin.c
cp build/plugins/hello.dll $D/v3x/plugins/ && printf '[plugins]\nHello = 0\nv4_ok=1\n' > $D/v3x/v3x.ini
$CC -o $D/api_v4_test.exe test/api_v4_test.c -Lbuild -l:version.dll && (cd $D && V3X_NO_CONSOLE=1 ./api_v4_test.exe)
python test/compat_check.py $T                 # needs $T/api_v4_test from above

API v5 and loader hardening (test/api_v5_test.c, modes: none, gate, gateload). Its plugin variants are -DV5_FRAME_FAULT, -DV5_CMD_FAULT, -DV5_EVENT_FAULT, -DV5_MAIN_FAULT, -DV5_LOAD_FAULT, -DV5_LOAD_FAIL, -DV5_RELOAD, -DV5_MARKED, -DV5_GATE_STAR/_VER/_PREFIX/_NONE of test/api_v5_plugin.c, each built as v5_<variant>.dll; the directory also needs a v3x.ini, loading.marker and fake crash.log (header of test/api_v5_test.c). The exact setup is in test/run_all.sh (not written yet), so no hand recipe is given here.

Compatibility (test/compat_test.c; mode old needs a v4 loader built from git 317e574, which run_all.sh will build):

D=$T/compat; mkdir -p $D/v3x/plugins && cp build/version.dll $D/
for v in 1 2 3 4 5; do $CC -shared -DCOMPAT_V=$v -o $D/v3x/plugins/compat_v$v.dll test/compat_plugin.c; done
$CC -o $D/compat_test.exe test/compat_test.c -Lbuild -l:version.dll -luser32 && (cd $D && V3X_NO_CONSOLE=1 ./compat_test.exe)

Process exit: every plugin plus the test-only test/shutdown_probe.c; the runner starts the child 20 times, each must exit within 10 s with code 42 after ExitProcess while other threads hammer the log lock, heap and CRT (con = own hidden console window with the input thread reading, nocon = V3X_NO_CONSOLE=1):

D=$T/shutdown && mkdir -p $D/v3x/plugins && cp build/version.dll $D/ && cp build/plugins/*.dll $D/v3x/plugins/
$CC -shared -o $D/v3x/plugins/shutdown_probe.dll test/shutdown_probe.c
$CC -o $D/shutdown_child.exe test/shutdown_test.c -Lbuild -l:version.dll -luser32
$CC -DSHUTDOWN_RUNNER -o $D/shutdown_test.exe test/shutdown_test.c
$D/shutdown_test.exe $D/shutdown_child.exe 20 con && $D/shutdown_test.exe $D/shutdown_child.exe 20 nocon

Installer

installer/test_installer.py runs V3X-Setup.exe (GUI-less --silent CLI) against fake game folders (<root>/<case>/Victoria 3/ with the first 4 KB of the real exe, so the build id is real); it never touches the real game. <root> is deleted at the end unless --keep.

python installer/make_dist.py                  # builds build/dist/V3X-Setup.exe
python installer/test_installer.py [--exe build/dist/V3X-Setup.exe] [--root D:/v3x_installer_test/run] \
                                   [--real-exe $EXE] [--keep]

Last reported result: 49/49 (installer worker, team chat #4374, 2026-11-13; not re-run for this page).