What it is
A native DLL that is injected into the running Three_Kingdoms.exe and
registers extra functions into the campaign Lua VM. It started from one problem
— moving an existing character between factions' recruitment pools without
creating a new character, which stock campaign Lua simply cannot do — and
has grown from there.
What the se.* API now covers:
- Characters and faction recruitment pools
- Retinues and units: recruit, replace, disband, strength, chevrons
- Character experience and skill points; assignments
- Faction progression, the Three Kingdoms and emperor seats
- Faction potential, CAI personality, diplomacy attitude
- Buildings and region slots; alliances and coalitions
- Effect bundles, income lines and horde income
- Auto-resolve outcomes, the main-menu build string
- Performance diagnostics and an AI recruitment policy
The full signature-level reference is on the se.* API reference page.
Install
Use the TK Mod Manager. That is
the supported path: tick Script extender in the launch panel and it
downloads script_extender.dll, checks the release's
manifest.json fingerprint against your installed executable, and
injects the DLL once the game reaches the main menu.
There is a command-line loader in the releases for development use:
injector.exe <dll> [Three_Kingdoms.exe]
Run it once the main menu is up. Building from source:
cargo build --release
which produces target/release/script_extender.dll and
injector.exe.
How it avoids breaking your game
The DLL fingerprints the executable by its PE TimeDateStamp and
SizeOfImage, and verifies every engine address it intends to use by
its first bytes before touching anything. On any mismatch it logs and
does nothing. A patched or updated game is therefore safe but inert — never
half-working.
- The on-disk executable is never patched. Everything happens in memory, in that one session.
- There is deliberately no generic peek / poke / call primitive. Only named, high-level operations are exposed.
- The internal natives (
se_recruit_unit,se_slot_construct, …) are not the public surface. Scripts use thesemodule only. - Saves made after using
se.modify.*carry the results — they are ordinary engine state. The DLL is not required to load such a save, only to keep using the API.
A healthy DLL log contains all N addresses and M vtables verified,
then lua_gettop hook installed, then one
registered se_* functions into lua_State … per Lua state.
Multiplayer
Campaign multiplayer is lockstep: every machine runs the same scripts and must make the same change at the same model tick. Since DLL 0.30 the API is built for that:
se.modify.*runs only inside a model callback in multiplayer. Called from a UI click, a timer or the console it is refused rather than queued — queuing would change only one machine's model and desync the game.- Nothing a synced script does may depend on which machine it runs on.
Do not branch on
cm:get_local_faction()oris_local_playerin code that ends in a modify call, and do not usemath.randomoros.timeto decide a change. - Both machines must run the same extender with the same simulation settings. The DLL forces its version and a hook fingerprint into the game's build string, which the lobby compares, so a player without the DLL or with a different one cannot join.
Session-only state — auto-resolver variables, redefined bundles, income lines, emperor policy — must be re-applied by the mod after loading, from a callback that runs on every machine.
Troubleshooting
The API is not there at all
type(se) ~= "table"means the DLL is not injected, or has not yet registered into this Lua state. Registration happens the first time a state ticks Lua: the main-menu state is served first, the campaign state once a campaign has loaded.- Check the DLL log for
fingerprint mismatch: expected …orN anchor(s) failed; refusing to run. That means the game was updated and the DLL has to be rebuilt against the new build.
Where the logs are
- DLL log:
script_extender.log, next to the injectedscript_extender.dll. Truncated on every injection and mirrored toOutputDebugString. - Lua log:
lua_mod_log.txtin the game root, plus the per-sessionironic_log_<faction>_<stamp>.txt— newest by modification time is the current one. - Crash dumps:
%LOCALAPPDATA%\CrashDumps.
The mod manager's Logs tab shows all of these without you having to find them.
Reporting a crash
Send the game build number, the DLL version (se.version()), the DLL
log, the minidump from %LOCALAPPDATA%\CrashDumps, the Lua log, and
the exact script that was running.
The natives validate vtables, back-pointers and registry round-trips and refuse instead of writing, so a refusal message is the expected outcome of a bad argument. A crash is a bug worth reporting — open an issue or say so in the Discord.
The API reference has the full table of refusal messages and what each one means.
License
MIT, the same as the mod manager. You are free to use, modify and redistribute it, including commercially, as long as the copyright notice and licence text come along.
Further reading
Two companion documents live in the repository rather than on this site, because they are a design writeup and a dated experiment log rather than reference material:
- AI_RECRUITMENT.md — how the vanilla campaign AI recruits, the measurements showing what it leaves on the table, and the design of the replacement policy.
- AI_RECRUITMENT_RUN_T1_T50.md — what that policy actually did to AI armies over 50 unattended turns.