---
name: enhanced-input
display_name: Enhanced Input System
description: Create and configure Enhanced Input — Input Actions, Mapping Contexts, key mappings, triggers, and modifiers (InputService). Use when the user asks to set up player input, create an Input Action (IA_) or Input Mapping Context (IMC_), bind keys to actions, or configure input triggers/modifiers.
vibeue_classes:
  - InputService
unreal_classes:
  - EditorAssetLibrary
---

> 🧠 **Brains complement:** IF an `unreal-engine-skills-manager` tool (external MCP) exists in this session, call it with `{action: "load", skill: "enhanced-input"}` for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.

# Enhanced Input Skill

## Critical Rules

### 🚨 Real Python method names — do NOT search for the docstring's "action" names

The `InputService` class docstring lists MCP-style action ids (`action_create`,
`mapping_list_contexts`, `action_configure`, ...). Those are **not** the Python method
names — `discover_python_class(method_filter=...)` matches nothing for them. The complete
real API is below; you rarely need a discovery call at all:

| Area | Methods |
|------|---------|
| Reflection | `discover_types()`, `get_available_keys(filter="")`, `get_available_modifier_types()`, `get_available_trigger_types()` |
| Actions | `create_action(name, path, value_type="Axis1D")`, `list_input_actions()`, `get_input_action_info(action_path)` → info or None, `configure_action(action_path, consume_input=True, trigger_when_paused=False, description="")`, `input_action_exists(action_path)` |
| Contexts | `create_mapping_context(name, path, priority=0)`, `list_mapping_contexts()`, `get_mapping_context_info(context_path)` → info or None, `mapping_context_exists(context_path)` |
| Mappings | `get_mappings(context_path)`, `add_key_mapping(context_path, action_path, key_name)`, `remove_mapping(context_path, mapping_index)`, `key_mapping_exists(context_path, action_path)` |
| Modifiers | `add_modifier(context_path, mapping_index, modifier_type)`, `remove_modifier(context_path, mapping_index, modifier_index)`, `get_modifiers(context_path, mapping_index)` |
| Triggers | `add_trigger(context_path, mapping_index, trigger_type, properties_json="")`, `add_action_trigger(action_path, trigger_type, properties_json="")` → JSON, `remove_trigger(context_path, mapping_index, trigger_index)`, `get_triggers(context_path, mapping_index)` |

`create_action` value types are the strings `discover_types()` returns: `"Boolean"`
(alias `"Digital"`), `"Axis1D"`, `"Axis2D"`, `"Axis3D"`. The `create_*` folder may be a bare
folder (`"Input"` goes under `/Game`) or start with its mount point (`/Game/...`, a plugin root);
`/Engine`, `/Script` and `/Temp` are refused, and so is an invalid asset name (empty, a space,
`.`), each with the reason in `error_message`. Modifiers and triggers are
addressed by **index on the mapping**, so `get_mappings` / `get_modifiers` /
`get_triggers` first, then add/remove by index.

### ⚠️ Property Names on Info Structs

| Struct | WRONG | CORRECT |
|--------|-------|---------|
| `InputTypeDiscoveryResult` | `value_types` | `action_value_types` |
| `KeyMappingInfo` | `key` | `key_name` |
| `InputModifierInfo` | `modifier_type` | `type_name` or `display_name` |
| `InputTriggerInfo` | `trigger_type` | `type_name` or `display_name` |

### ⚠️ Value Types

| Value Type | Use Case |
|------------|----------|
| `Boolean` | Simple press/release (Jump, Fire) |
| `Axis1D` | Single axis (Throttle, Zoom) |
| `Axis2D` | Two axes (Move, Look) |
| `Axis3D` | Three axes (3D manipulation) |

### ⚠️ Key Names

**Keyboard:** `SpaceBar`, `LeftShift`, `W`, `A`, `S`, `D`, `F1`, `Enter`, `Escape`, `BackSpace`...  
**Mouse:** `LeftMouseButton`, `RightMouseButton`, `MouseScrollUp`  
**Gamepad:** `Gamepad_FaceButton_Bottom`, `Gamepad_LeftThumbstick`, `Gamepad_LeftTrigger`, `Gamepad_RightTrigger`  
**Paired axes (use these for Axis2D bindings):** `Mouse2D` (mouse look), `Gamepad_Left2D` / `Gamepad_Right2D` (analog sticks)

Verify any other name with `get_available_keys("thumb")` (substring filter) instead of guessing.

### ⚠️ Triggers and Modifiers

**Triggers:** `Pressed`, `Released`, `Down`, `Hold`, `Tap`, `Pulse`  
**Modifiers:** `Negate`, `DeadZone`, `Scalar`, `SwizzleAxis` (also: `Smooth`, `SmoothDelta`, `ScaleByDeltaTime` — use `get_available_modifier_types()` for the full set)

---

## Workflows

### Create Input Action

```python
import unreal

result = unreal.InputService.create_action("Jump", "/Game/Input", "Boolean")
if result.success:
    unreal.EditorAssetLibrary.save_asset(result.asset_path)
```

### Create Mapping Context

```python
import unreal

result = unreal.InputService.create_mapping_context("Default", "/Game/Input", 0)
unreal.EditorAssetLibrary.save_asset(result.asset_path)
```

### Add Key Mapping

```python
import unreal

context_path = "/Game/Input/IMC_Default"
action_path = "/Game/Input/IA_Jump"
unreal.InputService.add_key_mapping(context_path, action_path, "SpaceBar")
unreal.EditorAssetLibrary.save_asset(context_path)
```

### Add Triggers and Modifiers

```python
import unreal

context_path = "/Game/Input/IMC_Default"
action_path = "/Game/Input/IA_Fire"

unreal.InputService.add_key_mapping(context_path, action_path, "LeftMouseButton")

# Get mapping index
mappings = unreal.InputService.get_mappings(context_path)
mapping_index = len(mappings) - 1

# Add trigger/modifier using mapping index
unreal.InputService.add_trigger(context_path, mapping_index, "Pressed")
unreal.InputService.add_modifier(context_path, mapping_index, "DeadZone")
unreal.EditorAssetLibrary.save_asset(context_path)
```

### Trigger Settings

`properties_json` sets the new trigger's settings, by C++ name or snake_case
(`HoldTimeThreshold` or `hold_time_threshold`, `bIsOneShot` or `is_one_shot`). Only the
settings the trigger's Details panel shows can be set: runtime state such as `HeldDuration`,
`LastValue` or `bShouldAlwaysTick` is refused ("not a trigger setting"), and a number outside
the property's `ClampMin`/`ClampMax` is refused with the allowed range (`hold_time_threshold`
must be >= 0). An unknown name, a refused setting or a bad value fails the call and adds
nothing. `add_action_trigger` puts the trigger on the Input Action itself, so it applies to
every mapping of the action; it returns `PIE_ACTIVE` while a play session runs (stop PIE
first). Neither saves the asset, and a running PIE session keeps its copy of the triggers:
restart PIE to see a change.

```python
import unreal

# Hold 0.4 s before the charged attack fires, once per hold
unreal.InputService.add_trigger("/Game/Input/IMC_Default", 0, "Hold",
    '{"hold_time_threshold": 0.4, "is_one_shot": true}')

# A quick tap on the action itself, for every key bound to it
result = unreal.InputService.add_action_trigger("/Game/Input/IA_Dash", "Tap",
    '{"tap_release_time_threshold": 0.25}')
print(result)   # {"success": true, "trigger_index": 0, ...} or an error_code
unreal.EditorAssetLibrary.save_asset("/Game/Input/IA_Dash")
```

### Get Mappings Info

```python
import unreal

mappings = unreal.InputService.get_mappings("/Game/Input/IMC_Default")
for m in mappings:
    print(f"Action: {m.action_name}, Key: {m.key_name}")

info = unreal.InputService.get_input_action_info("/Game/Input/IA_Jump")
if info:
    print(f"Action: {info.action_name}, ValueType: {info.value_type}")
```

### Discover Available Types

```python
import unreal

types = unreal.InputService.discover_types()
print(f"Value Types: {types.action_value_types}")
print(f"Modifiers: {types.modifier_types}")
print(f"Triggers: {types.trigger_types}")

keys = unreal.InputService.get_available_keys()
```

## Sample scripts (run via `execute_python_code`)

- **`scripts/setup_input.txt`** — create an Input Action + Mapping Context and bind a key.

## PIE input injection (issue #550)

Test input-driven gameplay WITHOUT remapping game assets or OS-level SendKeys — inject directly
into the running PIE session (no OS window focus needed):

```python
import unreal

# Enhanced Input action, QUEUED for the next input tick and released on the one after.
# X/Y/Z map to the value type. Read the result in a later call: nothing ticks while this one runs.
print(unreal.InputService.inject_action("/Game/Input/IA_Fire", 1.0))

# Hold an action for 1.5 s of real time (a guard, a charge, a Hold trigger); returns at once.
print(unreal.InputService.inject_action_for("/Game/Input/IA_Block", 1.5))
print(unreal.InputService.stop_injection("/Game/Input/IA_Block"))   # release early

# Raw key through Slate to the game viewport: "tap" (default), "down", "up", or "hold".
print(unreal.InputService.inject_key("SpaceBar"))
print(unreal.InputService.inject_key("RightMouseButton", "hold", 1.5))
```

All return JSON with `success`/`error_code` (PIE not running, `NO_LOCAL_PLAYER` /
`NO_PLAYER_CONTROLLER` while PIE or a client's join is still starting — retry on a later call —,
unknown key, `BAD_DURATION` outside 0.05 to 60 s). `inject_action` reports `queued: true`: it was
queued, not yet applied. The action calls take `pie_instance` to pick a PIE world: -1, the default,
is the first PIE world with a local player (the lowest instance number), and replies report the
instance actually used. A hold is one hold per action and PIE world whatever the path spelling, so
`stop_injection` finds it either way; holding a held key again extends it (`extended: true`); ending
PIE drops every hold. See the `pie-testing` skill for the full verification loop.
