Docs · For modders
include/v3x.h
The API header itself: plain C, versioned, append-only.
Copied from include/v3x.h in the V3X repository, file dated 2026-11-13.
/*
* V3X - native code loader for Victoria 3.
*
* A plugin is a normal Windows x64 DLL placed in <game>/binaries/v3x/plugins/.
* It must export:
*
* V3X_EXPORT int v3x_plugin_load(const V3X_API *api); // return 0 on success
*
* and may export:
*
* V3X_EXPORT const char *v3x_plugin_name = "my_plugin";
* V3X_PLUGIN_MANIFEST("my_plugin", "1.0.0", "me", "what it does", "release/1.15.1"); // API v4
*
* Full reference with thread rules: docs/api-reference.md. Safety rules: docs/safety.md.
* What is guaranteed to stay (ABI, return codes, versioning, game patches): docs/api-stability.md.
*
* v3x_plugin_load runs on the game's main thread, right before the game's own
* entry point (CRT startup, static constructors and WinMain have NOT run yet).
* Code is mapped and can be scanned and hooked; game globals are not set up yet.
* Use api->on_frame for work that needs a running game.
*
* Keep the API C-only so plugins can be built with any compiler (MSVC, clang, zig, gcc).
*/
#ifndef V3X_H
#define V3X_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Stability contract (docs/api-stability.md): every struct, field, constant and return value in
* this header is frozen. New things are only APPENDED (new V3X_API fields at the end, new
* constants); nothing is reordered, resized or removed, and a documented return value keeps its
* meaning. test/abi_test.c fails the build on any layout change; test/compat_test.c loads plugins
* built against the frozen v1..v4 headers (test/compat_v*.h) into the current loader. */
#define V3X_API_VERSION 5
#ifdef __cplusplus
#define V3X_EXPORT extern "C" __declspec(dllexport)
#else
#define V3X_EXPORT __declspec(dllexport)
#endif
/* ---- Return codes (unified names, API v5; the VALUES are those of v1-v4, unchanged) ----------
* Functions returning int use 0 for success. Negative = V3X's own errors; the hook functions
* (hook, unhook, hook_safe) also return MinHook MH_STATUS values (1..12) and V3X_EHOOKBUSY.
* patch returns a Win32 GetLastError() code. Exceptions: print returns the chars written,
* emit_event / enum_commands return a count, exec_* return 0 / -1. */
#define V3X_OK 0
#define V3X_EFAIL (-1) /* bad arguments, table/queue full, unknown command, or the process is
exiting; also MinHook's MH_UNKNOWN */
#define V3X_EEXIST (-2) /* register_command: name taken; on_event: same (name, fn, user) */
#define V3X_EMH_NOT_INITIALIZED 2 /* hooks: MinHook unavailable, or the process is exiting */
#define V3X_EMH_ALREADY_CREATED 3 /* hook_safe: this plugin already subscribed the same
pre/post/user there (a raw hook on a hooked target is V3X_EHOOKBUSY) */
#define V3X_EMH_NOT_CREATED 4 /* unhook: nothing hooked at target */
#define V3X_EMH_NOT_EXECUTABLE 7 /* target (or detour) is not executable memory */
#define V3X_EMH_UNSUPPORTED_FUNCTION 8 /* target's first bytes cannot be relocated */
#define V3X_EMH_MEMORY_ALLOC 9 /* no trampoline memory / thunk pool full / too many subscribers */
#define V3X_EMH_MEMORY_PROTECT 10 /* VirtualProtect failed */
/* ---- Log levels (API v5 log_at / log_level) ---- */
#define V3X_LOG_ERROR 1 /* line prefixed "ERROR: " (red in the console) */
#define V3X_LOG_WARN 2 /* "WARNING: " (red) */
#define V3X_LOG_INFO 3 /* no prefix; the default threshold */
#define V3X_LOG_DEBUG 4 /* "debug: "; hidden unless enabled in v3x.ini [log] */
#define V3X_LOG_TRACE 5 /* "trace: " */
/* ---- Event names emitted by the core and the bundled plugins (see on_event) ---- */
#define V3X_EVENT_PROCESS_EXIT "process_exit" /* data: const long * exit code */
#define V3X_EVENT_CONFIG_RELOAD "config_reload" /* v5, data: const char * plugin name or NULL = all */
#define V3X_EVENT_LOAD_TRANSITION "load_transition" /* data: const V3X_LoadTransition * */
#define V3X_EVENT_LOAD_TRANSITION_BEGIN "load_transition_begin"
#define V3X_EVENT_SAVE_BEGIN "save_begin" /* data: const V3X_SaveBegin * */
#define V3X_EVENT_SAVE_END "save_end"
typedef void (*v3x_frame_fn)(void *user);
typedef void (*v3x_command_fn)(const char *args, void *user);
/* ---- API v4: hook_safe ----------------------------------------------------------------------
* One call of a function hooked with api->hook_safe. The core's thunk fills this on the hooked
* thread's stack, calls pre(call), calls the original with the (possibly modified) arguments
* unless pre set skip_original, then calls post(call) and returns `ret` / `xmm_ret` to the caller.
*
* Win64 ABI: argument i (0-based) is in args[i] if it is an integer/pointer (rcx, rdx, r8, r9,
* then the caller's stack slots), in xmm[i] (i < 4) if it is a float/double. A struct > 8 bytes
* is passed as a pointer; a returned struct > 8 bytes uses a hidden pointer in args[0] (and
* every other argument shifts by one). args[4..15] are always copied from the caller's stack, even
* if the function takes fewer arguments (the extra slots are harmless garbage). Functions with more
* than 16 arguments are NOT forwarded correctly (do not hook_safe them). __vectorcall / HVA
* arguments are not supported. */
#define V3X_HOOK_MAX_ARGS 16
#define V3X_HOOK_MAX_SUBSCRIBERS 8 /* hook_safe subscribers per target (all plugins together) */
#define V3X_EHOOKBUSY 100 /* hook / hook_safe / unhook: the target is held by another plugin
(see v3x.log for the owner); outside the MH_STATUS range */
typedef union V3X_Xmm {
double d; /* a double argument / return value */
float f; /* a float argument / return value */
uint64_t u64;
uint32_t u32[4];
uint8_t b[16]; /* the full 128-bit register */
} V3X_Xmm;
typedef struct V3X_HookCall {
uint64_t args[V3X_HOOK_MAX_ARGS]; /* +0x00 integer args: rcx, rdx, r8, r9, stack args 5..16; pre may change them */
V3X_Xmm xmm[4]; /* +0x80 xmm0..xmm3 at entry (float args 1..4); pre may change them */
uint64_t ret; /* +0xc0 rax: the original's integer/pointer return value (0 before it runs) */
int32_t skip_original; /* +0xc8 pre sets 1: the original is not called; ret/xmm_ret are returned */
int32_t reserved; /* +0xcc */
V3X_Xmm xmm_ret; /* +0xd0 xmm0 after the original (float/double return value) */
void *user; /* +0xe0 the `user` given to hook_safe */
void *target; /* +0xe8 the hooked function */
void *original; /* +0xf0 trampoline to the original (read-only; call it only with
exactly the real prototype) */
void *return_address; /* +0xf8 the caller's return address (who called the function) */
uint64_t scratch[2]; /* +0x100 zeroed per call; free for pre -> post state (e.g. a start time) */
} V3X_HookCall;
/* pre/post callback of hook_safe. Runs on the thread that called the hooked function. */
typedef void (*v3x_hook_fn)(V3X_HookCall *call);
/* Optional plugin manifest (API v4), exported as data named `v3x_plugin_manifest`. The loader
* reads it before calling v3x_plugin_load and shows it in the `plugins` console command.
* Use V3X_PLUGIN_MANIFEST(...) below instead of filling it by hand. All strings may be NULL. */
#define V3X_MANIFEST_REQUIRE_BUILD 1u /* flags: refuse to load (v3x_plugin_load is not called)
when the running game matches none of game_builds */
typedef struct V3X_PluginManifest {
uint32_t struct_size; /* sizeof(V3X_PluginManifest) the plugin was built with */
uint32_t flags; /* V3X_MANIFEST_* */
uint32_t min_api; /* lowest loader api_version this plugin needs (0 = any); an older
loader refuses to load the plugin */
uint32_t reserved;
const char *name; /* e.g. "my_plugin" (wins over v3x_plugin_name) */
const char *version; /* e.g. "1.2.0" */
const char *author;
const char *description; /* one line */
const char *game_builds; /* ';'-separated list of supported games, each either a
game_version() string ("release/1.15.1"), a game_build_id()
("6af3094f-60fb000") or a prefix ending in '*' ("release/1.15*").
NULL, "" or "*" = any build. A mismatch only logs a WARNING
unless flags has V3X_MANIFEST_REQUIRE_BUILD. */
} V3X_PluginManifest;
#ifdef __cplusplus
#define V3X_MANIFEST_EXPORT_ extern "C" __declspec(dllexport)
#else
#define V3X_MANIFEST_EXPORT_ __declspec(dllexport)
#endif
/* V3X_PLUGIN_MANIFEST("my_plugin", "1.0.0", "me", "does things", "release/1.15.1") at file scope.
* V3X_PLUGIN_MANIFEST_EX adds flags and min_api. */
#define V3X_PLUGIN_MANIFEST_EX(name_, version_, author_, desc_, builds_, flags_, min_api_) \
V3X_MANIFEST_EXPORT_ const V3X_PluginManifest v3x_plugin_manifest = { \
(uint32_t)sizeof(V3X_PluginManifest), (flags_), (min_api_), 0u, (name_), (version_), (author_), \
(desc_), (builds_)}
#define V3X_PLUGIN_MANIFEST(name_, version_, author_, desc_, builds_) \
V3X_PLUGIN_MANIFEST_EX(name_, version_, author_, desc_, builds_, 0u, 0u)
typedef struct V3X_API {
uint32_t api_version; /* V3X_API_VERSION the loader was built with */
uint32_t struct_size; /* sizeof(V3X_API) of the loader; check before using newer fields */
/* Logging to <game>/binaries/v3x/v3x.log. `tag` is usually your plugin name. */
void (*log)(const char *tag, const char *fmt, ...);
/* Main module (victoria3.exe) */
uintptr_t (*image_base)(void);
size_t (*image_size)(void);
const char *(*game_version)(void); /* contents of caligula_branch.txt, e.g. "release/1.15.1" */
/* Finding engine code without hardcoded addresses.
* pattern_scan: IDA-style signature, e.g. "48 8B 05 ?? ?? ?? ?? 48 85 C0", searched in .text.
* find_string: address of the exact NUL-terminated ASCII string in .rdata/.data.
* find_xref: the index-th instruction in .text that references `target` with a
* RIP-relative lea/mov (index 0 = first).
* function_start: start of the function containing `addr` (from the .pdata unwind table).
* All return NULL when nothing is found. */
void *(*pattern_scan)(const char *ida_pattern);
void *(*find_string)(const char *s);
void *(*find_xref)(const void *target, int index);
void *(*function_start)(const void *addr);
/* Inline hooking (MinHook). `original` receives a callable trampoline.
* Return 0 on success, otherwise a MinHook MH_STATUS code.
* A raw hook is EXCLUSIVE: on a target that already has a raw hook or hook_safe subscribers
* (from any plugin) hook returns V3X_EHOOKBUSY and v3x.log names the owner. Prefer hook_safe
* for engine functions other plugins may want too: it can be shared.
* unhook removes only what the CALLING module (plugin DLL, found from the return address)
* holds on `target`: its raw hook, or all of its hook_safe subscriptions there (other plugins'
* subscriptions stay; the thunk is disabled and the original bytes restored when the last
* subscriber leaves). Returns V3X_EHOOKBUSY if only other plugins hold it,
* MH_ERROR_NOT_CREATED (4) if nothing is hooked. */
int (*hook)(void *target, void *detour, void **original);
int (*unhook)(void *target);
/* Write bytes over code or read-only data (handles page protection + icache). 0 on success. */
int (*patch)(void *addr, const void *bytes, size_t n);
/* Called once per message-pump iteration on the game's main thread (about once per frame). */
int (*on_frame)(v3x_frame_fn fn, void *user);
/* Absolute paths with trailing backslash. */
const char *(*game_dir)(void); /* .../Victoria 3/binaries/ */
const char *(*v3x_dir)(void); /* .../Victoria 3/binaries/v3x/ */
/* ---- API v2 (check api_version >= 2 or struct_size before using) ----
* Interactive console. The V3X console window reads lines on a background input thread
* and dispatches "name args..." to registered commands. Without a console window
* (console=0 / V3X_NO_CONSOLE=1) registration still succeeds, commands just never run.
*
* Built-in commands: help, plugins, log <text>, version, quit (only prints a note: V3X never
* exits the game). Ctrl+C in the console is swallowed so it cannot kill the game.
*
* register_command: `fn` runs on the console input thread (NOT the game's main thread) with
* the rest of the line after the name (leading spaces stripped, never NULL). Names are
* case-insensitive, up to 31 chars. Returns 0, -1 table full / bad args, -2 name taken.
* print: printf to the console (plain, no tag) and v3x.log; a trailing newline is added if
* missing. Output over 8 KB is truncated. Returns chars written or -1. Thread-safe.
* run_on_main: queue fn(user) to run once in the next on_frame tick on the main thread
* (use it from commands that touch game state). Returns 0, or -1 if the queue is full.
* exec_command: run one console line synchronously on the calling thread, as if typed.
* Returns 0 if a command ran, -1 if the name is unknown/empty (loader 0.5.0: or its plugin was
* disabled after a fault; a command that faults is isolated and still returns 0).
* Loader 0.5.0: on_frame, run_on_main, command and event callbacks (and v3x_plugin_load) run inside
* a fault guard; a fault disables that plugin's callbacks (docs/api-reference.md "Fault isolation"). */
int (*register_command)(const char *name, const char *help, v3x_command_fn fn, void *user);
int (*print)(const char *fmt, ...);
int (*run_on_main)(v3x_frame_fn fn, void *user);
int (*exec_command)(const char *line);
/* ---- API v3 (check api_version >= 3 or struct_size before using) ----
* exec_capture: like exec_command (synchronous, on the calling thread, same return value), and
* while the command runs, every api->print and api->log made ON THE CALLING THREAD is also
* passed to sink(text, user). Output still goes to the console and v3x.log as usual; output
* from other threads (e.g. threads the command starts) is not captured.
* text: for print, the printed text with its trailing '\n'; for log, the full log line
* ("[hh:mm:ss.mmm][tag] message\n"). One sink call per print/log call.
* The sink runs on the calling thread; print/log made from inside the sink are not fed back
* to it. Nested exec_capture calls use the inner sink and restore the outer one afterwards.
* sink may be NULL (then it is exactly exec_command). */
int (*exec_capture)(const char *line, void (*sink)(const char *text, void *user), void *user);
/* Named events between plugins (one plugin owns a hook and emits, others subscribe).
* on_event: subscribe fn(name, data, user) to `name` (case-sensitive, 1..63 chars). The table is
* fixed at 64 subscriptions for all plugins together; the same (name, fn, user) may be
* subscribed only once. Returns 0, -1 table full / bad args, -2 already subscribed.
* emit_event: calls every subscriber of `name` synchronously on the emitting thread, in
* subscription order, with `data` passed through untouched (meaning defined per event).
* Subscribers may subscribe or emit from inside the callback (a subscriber added during an
* emit is called from the next emit on). Returns the number of subscribers called, -1 on bad args.
* Known events (emitter, thread, data):
* "load_transition" fast_load, main thread, when a load-state transition ENDS;
* data = const V3X_LoadTransition * (may be NULL).
* "load_transition_begin" fast_load, main thread, when a transition BEGINS;
* data = const V3X_LoadTransition * (seconds = 0; may be NULL).
* "save_begin" enhance, the saving thread (main), right BEFORE the engine serializes
* a save (SaveGameState: autosave, exit save, console, save dialog);
* data = const V3X_SaveBegin *.
* "save_end" enhance, same thread, right after that save call returned;
* data = the same const V3X_SaveBegin *.
* "process_exit" core, the thread calling ExitProcess, once, BEFORE the OS kills the other
* threads (they all still run); data = const long * (exit code). The place
* for final reports and for stopping anything that suspends threads. Keep
* it short; never wait for another thread without a timeout.
* Process exit rule: in DllMain(DLL_PROCESS_DETACH) with reserved != NULL every other thread is
* already dead (maybe holding a lock): do nothing there. From then on log/print/hook/run_on_main/
* emit_event/exec_command/register_command/on_event return at once without effect. */
int (*on_event)(const char *name, void (*fn)(const char *name, void *data, void *user), void *user);
int (*emit_event)(const char *name, void *data);
/* ---- API v4 (check with V3X_HAS(api, field) before using) ----
* hook_safe: hook `target` without writing a detour. The core generates a pass-through thunk
* that saves all 16 integer argument slots (rcx, rdx, r8, r9 + 12 stack slots) and xmm0-xmm3,
* calls pre(call), forwards everything to the original (unless call->skip_original), keeps
* rax and xmm0, calls post(call) and returns them. pre or post may be NULL (not both). The thunk
* has unwind info, so exceptions and stack walks pass through it. Callbacks run on whatever
* thread calls the hooked function (often an engine worker): keep them short, and never run
* game script from them unless you know you are inside a tick task (sdk/v3x_game.h
* v3x_script_random_allowed). Returns 0 or a MinHook MH_STATUS like hook (9 =
* MH_ERROR_MEMORY_ALLOC also when the thunk pool is full or the target already has
* V3X_HOOK_MAX_SUBSCRIBERS subscribers, 3 = MH_ERROR_ALREADY_CREATED when this plugin already
* subscribed the same pre/post/user there, V3X_EHOOKBUSY when the target has a raw hook,
* -1 bad args). Remove with unhook; the thunk itself is never freed (a thread may still be
* inside it). Any thread.
* Several plugins (or one plugin several times, with different pre/post/user) may hook_safe
* the SAME target: they share one thunk and run nested, like wrappers. pre callbacks run in
* subscription order, then the original, then post callbacks in REVERSE order. Each one gets
* its own call->user and its own call->scratch; args / ret changes are seen by the next one.
* A pre that sets skip_original ends the chain for that call: the later subscribers' pre and
* post do not run, the original is not called, the post callbacks of the earlier subscribers
* and of the skipping one still run (so the first subscriber to skip wins). Subscribing and
* unsubscribing are safe while other threads call the target; a call that already started
* finishes with the subscriber list it started with.
* main_thread_id: Win32 thread id of the game's main thread (the one on_frame/run_on_main use).
* game_build_id: identity of the running victoria3.exe from its PE header,
* "<TimeDateStamp hex>-<SizeOfImage hex>", e.g. "6af3094f-60fb000". It changes with every
* game patch, even when caligula_branch.txt (game_version) does not.
* enum_commands: calls fn(name, help, user) for every registered console command in
* registration order (outside the command lock; fn may register commands). Returns the
* number of commands, -1 on bad args. Any thread. */
int (*hook_safe)(void *target, v3x_hook_fn pre, v3x_hook_fn post, void *user);
uint32_t (*main_thread_id)(void);
const char *(*game_build_id)(void);
int (*enum_commands)(void (*fn)(const char *name, const char *help, void *user), void *user);
/* ---- API v5 (check with V3X_HAS(api, field) before using) ----
* log_at: like log, with a level (V3X_LOG_ERROR..V3X_LOG_TRACE). The line is written only if
* level <= log_level(tag); ERROR/WARN/DEBUG/TRACE lines get the prefix "ERROR: " / "WARNING: " /
* "debug: " / "trace: " after the tag. Levels outside 1..5 are clamped. Any thread.
* log_level: the current threshold for `tag` (NULL = the global one): v3x.ini [log] "<tag> = <level>"
* if present, else [log] "level = <level>", else env V3X_LOG_LEVEL, else V3X_LOG_INFO. Levels are
* names (error, warn, info, debug, trace) or numbers 1-5. The console command
* `loglevel [tag] <level>` changes it at runtime. Use it to skip expensive formatting:
* if (api->log_level("me") >= V3X_LOG_DEBUG) dump(); Any thread, lock-free.
* loader_version: semantic version of the loader build (version.dll), e.g. "0.5.0". Independent
* of api_version (the table layout) and of game_version / game_build_id (the game). Any thread.
* build_matches: 1 if the running game matches the ';'-separated list (the exact rules of the
* manifest's game_builds: game_version() or game_build_id(), case-insensitive, trailing '*' =
* prefix, NULL / "" / "*" = any), else 0. For plugins that gate single features per build.
* Any thread.
* Core event V3X_EVENT_CONFIG_RELOAD ("config_reload"): the console command `reload [plugin]`
* emits it on the MAIN thread (next frame tick) with data = const char * plugin name, or NULL
* for `reload` without a name (= every plugin). Re-read your ini in the handler if data is NULL
* or equals your name (sdk/v3x_util.h v3x_reload_is_for). */
void (*log_at)(int level, const char *tag, const char *fmt, ...);
int (*log_level)(const char *tag);
const char *(*loader_version)(void);
int (*build_matches)(const char *game_builds);
} V3X_API;
/* True if the loader's API table has `field` and it is set (the loader was built with a header
* that has it). Example: if (V3X_HAS(api, hook_safe)) api->hook_safe(...); */
#define V3X_HAS(api, field) \
((api) && (api)->struct_size >= offsetof(V3X_API, field) + sizeof((api)->field) && (api)->field)
/* data of the "load_transition" event */
typedef struct V3X_LoadTransition {
const char *from, *to; /* state names, may be NULL */
double seconds; /* time spent in `from` */
} V3X_LoadTransition;
/* data of the "save_begin" / "save_end" events */
typedef struct V3X_SaveBegin {
const char *name; /* save name as the engine passes it (NUL-terminated copy, valid only during the
callback; exact form, e.g. with or without ".v3", not verified in game); may be NULL */
} V3X_SaveBegin;
typedef int (*v3x_plugin_load_fn)(const V3X_API *api);
/* version.dll also exports `const V3X_API *v3x_api(void)` (for tests and tools that are not
* plugins, e.g. GetProcAddress(GetModuleHandleA("version.dll"), "v3x_api")). */
typedef const V3X_API *(*v3x_api_fn)(void);
/* Since loader 0.5.0 (API v5) version.dll also exports two data symbols that tools can read
* from the file without loading it (sdk/build_plugin.py check):
* const uint32_t v3x_loader_api; = V3X_API_VERSION of that loader
* const char v3x_loader_semver[]; = loader_version(), e.g. "0.5.0"
* const char v3x_loader_known_builds[]; ';'-separated game_build_id()s the loader was tested on;
* on any other victoria3.exe the unknown-build gate applies */
#ifdef __cplusplus
}
#endif
#endif