An API mod for X4: Foundations that lets other mods register custom hotkey actions - bindable by the player through the native game keybinding UI, with no external process and no dependency on SirNukes Mod Support APIs' own Hotkey API.

Overview
Specifics of X4's input system: it uses specialized INPUT_ACTION_* enums to assign physical keys to in-game actions. These enums are compiled into the game and cannot be extended by mods.
This mod reclaims 48 otherwise-unused debug-only action slots and exposes them as a registration API: a consumer mod asks for an id, the framework hands it the next free slot from the pool, and the player assigns the actual physical key through the same native remap UI (and conflict-detection) every other control uses.
So, this mod will work till the Egosoft wil not remove the debug action slots from the game or will do anything else to break this functionality.
Two integration paths exist, both calling into the same registration/dispatch core:
- MD API - for Mission Director scripts. Uses Register_Action / Reloaded cue signals; the registered callback is an MD cue.
- Lua API - for Lua UI scripts. Call HotkeyApi.RegisterAction(...) directly; the registered callback is a Lua function, invoked with no MD/blackboard round trip at all.
Requirements
- X4: Foundations: Version 8.00HF4 or higher and UI Extensions and HUD: Version v8.0.4.9 or higher by kuertee.
- Available on Nexus Mods: UI Extensions and HUD
- X4: Foundations: Version 9.00 or higher and UI Extensions and HUD: Version v9.0.0.6 or higher by kuertee.
- Available on Nexus Mods: UI Extensions and HUD
- Steam Workshop: Native Hotkey API
- Nexus Mods: Native Hotkey API
A Hotkey Management entry is added to the top-level Options menu, right before Settings.
Important: this menu is only available when the save is loaded or new game is started, i.e. is not accessible from the main menu!
Spoiler
Show

Spoiler
Show

- Hotkey Bindings - the actual keybinding page (native remap UI: click a row, press a key, confirm/replace on conflict). Conflicts are checked both against other hotkeys registered through this API and against vanilla bindings on other Controls pages.
Spoiler
Show

Spoiler
Show

Spoiler
Show

- Hotkey Requests - lists every id any mod has tried to register this session, regardless of whether it currently holds a slot, with a checkbox per row:
Spoiler
Show

- Checked (default) - enabled; holds a slot if one was free when it last registered.
- Unchecked - blocked: the slot (if any) and its key binding are freed immediately, available for some other id to claim.
- Status column shows Available (holds a slot, no key bound yet), Assigned: \<key\> (holds a slot and a key), Waiting (pool full) (enabled, but all 48 slots were taken last time it tried), or Blocked.
- Re-enabling a blocked id does not immediately reclaim a slot - press Apply to re-run registration for everyone (this also re-checks for orphaned slots, see Clearance below).
- A debug-logging toggle (Enabled/Disabled).
Flow
- Your cue listens to md.HotkeyApi.Reloaded
- Call md.HotkeyApi.Register_Action with your own callback cue
- Re-send Register_Action every time Reloaded fires again (cue references
- When the player presses the assigned key, your callback cue is signalled
- $id (string) - unique identifier of this action, chosen by your mod. The same id always maps to the same physical hotkey slot once assigned.
- $version (number, optional, default 1) - protocol version this request was built for. A version newer than this build supports is rejected outright rather than risk being silently misinterpreted.
- $area (string, mandatory, no default) - one or more of 'map', 'pilot', 'fps', separated by ; if more than one (e.g. 'map;pilot') - where the action is allowed to fire and what "selected object" means for $isObjectRequired. 'pilot' means the player is piloting a ship (no menu open). 'fps' means the player is on foot/first-person - not in any menu, and not piloting a ship in space (e.g. walking around a station or ship interior). Each area value has its own minimum supported $version (currently 'map', 'pilot', and 'fps' are all available from version 1) - a future area value added in a later version is only honored for a request that itself declares at least that version.
- $isObjectRequired (bool, optional, default false) - if true, the action is skipped unless a map selection ('map') or ship target ('pilot') is currently present. Currently skipped for 'fps'.
- $name (string) - display name shown for this action's row on the Hotkey Bindings/Hotkey Requests pages.
- $actionCue (cue reference) - signalled when the hotkey fires and any target requirement is satisfied. Receives param = table[$id = id], plus $object (an MD component reference) if a selection/target was present.
A shared library, include_actions ref="md.HotkeyApi.GetDebugChance", computes $debugChance (0 or 100) from the player's current debug-logging preference (toggled from the Hotkey Requests page). Any cue - in this mod or a consumer's - can include it once and then use chance="$debugChance" on its own debug_text calls, so toggling the option silences everything in lockstep instead of each mod tracking its own flag. Cross-extension references must be fully qualified (ref="md.HotkeyApi.GetDebugChance") - MD library references are namespaced per-script, not globally bare-addressable.
Usage example MD
Code: Select all
<cue name="Register_My_Action" instantiate="true">
<conditions>
<event_cue_signalled cue="md.HotkeyApi.Reloaded" />
</conditions>
<actions>
<signal_cue_instantly
cue="md.HotkeyApi.Register_Action"
param="table[
$id = 'my_mod_my_action',
$area = 'map;pilot',
$isObjectRequired = false,
$name = 'My Action',
$actionCue = My_Action_Cue,
]"/>
</actions>
</cue>
<cue name="My_Action_Cue" instantiate="true" namespace="this">
<conditions>
<event_cue_signalled />
</conditions>
<actions>
<debug_text text="'My action fired! param: %s'.[event.param]" chance="100" filter="general" />
</actions>
</cue>
Flow (Lua)
- Listen for the "HotkeyApi.Register_Request" event (raised every time
- Call HotkeyApi.RegisterAction({...}) in response, every time
- Your actionLua function is called directly when the assigned key is
HotkeyApi.RegisterAction - request fields
Same fields as MD's Register_Action (using plain Lua values, no $ prefix), plus:
- actionLua (function) - called as actionLua({id, object}) when the hotkey fires and any target requirement is satisfied. object is the selected/targeted component id (a real Lua value, not a blackboard round trip), or nil if there wasn't one.
Usage example Lua
Code: Select all
RegisterEvent("HotkeyApi.Register_Request", function()
HotkeyApi.RegisterAction({
id = "my_mod_my_action",
area = "map;pilot",
isObjectRequired = false,
name = "My Lua Action",
actionLua = function(params)
DebugError("My action fired! id: " .. tostring(params.id))
end,
})
end)
Every slot loaded from a previous session starts each reload marked unconfirmed; registering again marks it confirmed. Ten seconds after every Reloaded broadcast (long enough for every consumer to have responded), a Clearance sweep clears the key binding and frees the slot of anything still unconfirmed - e.g. a mod that was removed, or stopped registering that id. This runs automatically; no action needed from consumers.
Limitations
- 48-action hard ceiling. INPUT_ACTION_* is a closed enum compiled into the game; this pool cannot be extended.
- Cross-page conflict checking is conservative, not area-aware. Remapping one of this pool's keys checks for conflicts against every other Controls page, regardless of the hotkey's own area - this was deliberately simplified after testing showed per-area filtering wasn't reliable to scope correctly.
- This conservative behavior in addition means that if a player assigns, despite a warning, a key combination that is already bound to a vanilla action, this combination will not be accepted and the player will have to reassign some other key.
- Loading a save without some of the mods that registered hotkeys will clear their slots and key bindings, so the player will have to reassign them when they reload with those mods again.
- The keybindings in X4 are not saved in the save file, but stored per "profile". Please note that the keybindings created for one game will be "replicated" to any other save using the same profile.
- Author: Chem O`Dun, on Nexus Mods and Steam Workshop
- "X4: Foundations" is a trademark of Egosoft.

