Architecture¶
CAPIE has four layers. Each one depends only on the layer below it, and the bottom layer, the game itself, is never modified.
Dependent mods (Rename Anything, others)
call SDK effects and triggers, ship the indicator snippet
CAPIE script SDK (plain HOI4 script, versioned file names)
capie_v1_* triggers, effects, scripted loc, loc keys
fallback branches, authority checks, cooldowns, validation
CAPIE modules (native)
text transport | name layers | UI refresh
status loc keys | version record for multiplayer | console commands
CAPIE core (native)
game build check (SHA-256) | signature table | hook manager
input validation | capability and version registry | logging
Unmodified HOI4 (hoi4.exe and steam_api64.dll untouched)
Core¶
The core decides whether CAPIE may run at all.
- Game build check. CAPIE computes the SHA-256 of the running
hoi4.exewith Windowsbcryptand compares it with the pinned build. Works today - Pattern check. Every place CAPIE hooks must match a byte pattern exactly once, at the expected address, before any hook is switched on. Works today
- Hook manager. CAPIE uses MinHook. It enables all hooks or none, and it keeps trampolines until the process exits, because a call may still be running through one. Works today
- Signature table. Every signature and pin is one row of a table in the source, checked by one matcher and by an offline test against the game file. Works today A signed table per supported game build is Planned.
- Capability and API level.
capie_core.dllexports a capability value that is non-zero only after a successful start, and every annotated command carries the API level ascapie_cap. Works today
This is the pattern check of the first country module, from before CAPIE; the current matcher
in plugin/capie/src/core/signatures.hpp applies the same rules to a table of signatures:
struct HookSpec { size_t rva; std::vector<int> pattern; void* replacement; void** original; };
bool unique_signature(const unsigned char* base, const IMAGE_SECTION_HEADER* text,
const HookSpec& hook) {
const auto* start = base + text->VirtualAddress;
const size_t size = text->Misc.VirtualSize;
size_t matches = 0;
for (size_t off = 0; off + hook.pattern.size() <= size; ++off) {
bool match = true;
for (size_t i = 0; i < hook.pattern.size(); ++i) {
if (hook.pattern[i] >= 0 && start[off+i] != hook.pattern[i]) { match = false; break; }
}
if (match) {
if (static_cast<size_t>(text->VirtualAddress) + off != hook.rva || ++matches > 1) return false;
}
}
return matches == 1;
}
A pattern entry of -1 is a wildcard for bytes that change when the game is relinked, such as
relative addresses. A second match anywhere in the code section, or a match at the wrong
address, fails the check.
Start-up errors are numbered, so a log line tells you which check stopped CAPIE:
| Code | Meaning |
|---|---|
| 0 | Active |
| 10 | Unsupported hoi4.exe hash |
| 11 | Wrong executable format or machine type |
| 12 | Code section not found |
| 20 | A pattern did not match exactly once at its address |
| 30, 31, 32 | MinHook could not initialise, create or enable the hooks |
Modules¶
| Module | What it does | State |
|---|---|---|
| Name layers | Puts a country's custom name and adjective in place while the game rebuilds its name caches, without touching the cosmetic tag or flag (Name layers); still reads the older RN1\|Name\|Adjective\| cosmetic tag |
Works today on a developer build; faction names In progress |
| Status loc keys | Overrides reserved localisation keys when they are drawn; proven with RN_PLUGIN_STATUS |
Works today for the mechanism; CAPIE_* keys built, need an in-game test |
| Text transport | Copies the text of a known native text field into temporary records of the player's own Apply command | Works today for Rename Anything's fields and for grammar editors of other mods |
| Capability records | Adds the API level to known commands and removes script-set copies | Works today as rn_native_cap and capie_cap |
| UI refresh | Redraws the leader's name in the open Politics panel after a successful rename | Works today |
| Version record | Adds the CAPIE version (capie_ver) to every annotated command, for the multiplayer check |
Built, needs a two-machine test |
| Console commands | change_nation_name, reset_nation_name and capie_cmd in the game's console |
Works today in developer builds only |
Modules are compiled into two DLLs, not loaded at run time: capie_core.dll (names, status,
capability) and capie_editor.dll (editors, refresh). The launcher's hashes of the two files
cover all native code.
One rename, end to end¶
- The player opens an editor and types into the game's own text field (
editBoxType). - On Apply or Enter, the game builds the scripted GUI command. CAPIE copies the field's text into temporary records of that command, as character codes, and adds the capability record.
- The game runs the command's effect. The script checks the capability record, checks authority, decodes and validates the text, then calls a vanilla setter. For a country it writes the name layer: country variables with the character codes of the name and adjective.
- When the game rebuilds the country's cached names, CAPIE puts the layer in place of the tag the names are built from. Without CAPIE the game builds the names as usual and shows the cosmetic tag or base name.
Principles¶
- Minimal intrusion: hooks only where a script cannot do the job, each forwarding to the original exactly once.
- Fail closed: an unknown build, an ambiguous pattern, bad input or a missing capability means CAPIE does nothing and the script takes its fallback branch.
- Full passthrough: anything CAPIE does not recognise exactly goes to the game unchanged.
- No network, no files: CAPIE opens no sockets and does no file input or output on behalf of scripts.
- Script is the source of truth: names live in the save through vanilla mechanisms. CAPIE keeps no sidecar files and no state between sessions.
- Visible degradation: when CAPIE is missing or too old, the player sees it.