Docs · For modders
Extending game script from a V3X plugin
Ship game script, localization and GUI files from a native plugin.
Copied from docs/scripting.md in the V3X repository, file dated 2026-11-13.
This guide is for modders who want a native V3X plugin to add game script (effects, triggers, script values, events, on_actions), localization or GUI. Engine background: docs/re/script.md.
What works today
| Layer | Status |
|---|---|
Ship script, loc and GUI files from a plugin (script_ext, sdk/v3x_script.h) |
implemented, tested offline (test/script_ext_test.c), not run in game yet |
| Run effect or trigger text from C at runtime | not available (see the end of this guide) |
New script keywords implemented in C (v3x_<name> = ...) |
not available (see the end of this guide) |
Shipping files covers most “add something to script” cases. Anything a mod can define, a plugin can now define too: scripted effects and triggers, script values, events, decisions, journal entries, on_actions, defines, localization and GUI. The plugin can also generate those files at load, for example from its ini, the player’s settings or data computed in C.
How it works
- Your plugin’s
v3x_plugin_loadruns before the game starts. It callsscript_extto write files intobinaries/v3x/content_packs/<pack>/. - The
content_packsplugin mounts every pack into the game’s virtual file system when the VFS initializes. That happens after all plugins are loaded and before any script is read (docs/re/vfs.md). - The engine reads
common/...,events/...,localization/...andgui/...from the pack exactly as it reads a mod’s files.
Priority: packs sit above game/ and below Paradox mods (mods are mounted later and go on top).
A new file name (for example common/scripted_effects/my_plugin_effects.txt) adds to the game; it
does not replace anything. A file with the same path as a vanilla file replaces the vanilla file.
Both script_ext and content_packs must be installed and enabled.
API (sdk/v3x_script.h)
const V3X_ScriptExt *sx = v3x_script_get(api); /* NULL = script_ext.dll not installed */
sx->begin_pack("my_pack"); /* creates the pack, deletes files YOU wrote there last session */
sx->write_file("my_pack", "relative/path.txt", data, size);
sx->end_pack("my_pack"); /* writes the pack's manifest; returns the file count */
sx->packs_dir(); /* ...\binaries\v3x\content_packs\ */
Rules:
- Call it from
v3x_plugin_load. Files written later in the session are not read: the game loads its script databases once, at startup. - Pack names may contain letters, digits,
_,-and., up to 63 characters, and must not start with_or.(content_packs skips such folders). Paths are relative (/or\) with no..and no drive letter, up to 200 characters. Anything else returnsV3X_SCRIPT_EARG. *.ymlfiles get a UTF-8 BOM if they lack one, because the engine needs it for localization.- Files are replaced atomically. A file you stop writing is deleted at the next
begin_pack. Files in the pack that were not written byscript_extare never touched. - Prefix your names (
my_plugin_...). Vanilla uses nov3x_names (checked by the test against 1.15.1), sov3x_is free for V3X’s own content. - In the V3X console,
scriptextlists the packs written this session.
Complete example: v3x_hello logs the country every month
The plugin adds:
- a scripted effect
v3x_hello - an on_action that runs it for every country each month
- one localization key
The effect logs the scopes with the engine’s own debug_log / debug_log_scopes (output goes to
Documents/Paradox Interactive/Victoria 3/logs/debug.log).
/* plugins/hello_script/hello_script.c - build: python sdk/build_plugin.py plugins/hello_script */
#include <string.h>
#include "v3x.h"
#include "v3x_script.h"
V3X_EXPORT const char *v3x_plugin_name = "hello_script";
static const char EFFECTS[] =
"# generated by hello_script\n"
"v3x_hello = {\n"
" debug_log = \"v3x_hello ran\"\n"
" debug_log_scopes = yes\n"
"}\n";
static const char ON_ACTIONS[] =
"# appends to the vanilla on_action (root = country)\n"
"on_monthly_pulse_country = {\n"
" on_actions = { v3x_hello_monthly }\n"
"}\n"
"v3x_hello_monthly = {\n"
" effect = { v3x_hello = yes }\n"
"}\n";
static const char LOC[] =
"l_english:\n"
" v3x_hello_title: \"Hello from V3X\"\n";
V3X_EXPORT int v3x_plugin_load(const V3X_API *api)
{
const V3X_ScriptExt *sx = v3x_script_get(api);
if (!sx) { api->log("hello_script", "script_ext not installed - nothing written"); return 0; }
if (sx->begin_pack("hello_script") != V3X_SCRIPT_OK) return 0;
sx->write_file("hello_script", "common/scripted_effects/v3x_hello_effects.txt", EFFECTS, strlen(EFFECTS));
sx->write_file("hello_script", "common/on_actions/v3x_hello_on_actions.txt", ON_ACTIONS, strlen(ON_ACTIONS));
sx->write_file("hello_script", "localization/english/v3x_hello_l_english.yml", LOC, strlen(LOC));
api->log("hello_script", "wrote %d file(s)", sx->end_pack("hello_script"));
return 0;
}
Check it in game (not yet done by us):
debug.loghasv3x_hello ranonce per country per month.error.loghas no script errors for these files.- The console command
script_docslistsv3x_helloamong the scripted effects.
A scripted effect only runs in the game’s own script context (tick tasks, events, on_actions, decisions). That is the safe place for script (docs/safety.md “Script and RNG scope”). You never run it yourself from a hook or a frame callback.
Open point to check in game: the example assumes that a second on_monthly_pulse_country
definition merges its on_actions list into the vanilla one. That is the usual Jomini
behaviour, but it is not verified for 1.15.1. If it does not merge, trigger your on_action from an
event or a decision instead, or ship a copy of the vanilla on_action file with your line added. A
copy of a vanilla file breaks when the game patches that file.
Not available (and why)
- Running effect/trigger text from C and C-implemented keywords are not implemented. The engine internals they would use are described in docs/re/script.md sections 1-5: the Script Runner parse/execute functions, game-state access, the random-scope wrapper, and the effect/trigger registries filled by static initializers. During implementation of these two layers the worker’s output was stopped by a safety check, and the work was not continued. The CTO decides whether and how these layers are added.
- Until then, use script itself for logic. Communicate from C to script by generating files at load
(values, lists, effects built from your data). Communicate from script to C by reading what script
writes to the logs (
debug_log), or through game state your plugin already reads.
Files
sdk/v3x_script.h: API and thev3x_script_gethelperplugins/script_ext/script_ext.c: plugin (exportsv3x_script_ext, consolescriptext)plugins/script_ext/sx_pack.h: file writertest/script_ext_test.c: offline test. It also checks that the RE anchors in docs/re/script.md still match the exe.tools/re_script_rtti.py: RTTI vtable list used for the RE