# Input Pipeline

V4 is one game that has to feel like six. A tactical-FPS operator twin-sticking
between cover, a social-stealth assassin tapping a single light-out button, a
real-time-tactics commander dragging a cursor over a floor plan, a Wukong-class
action-RPG hero charging a quarter-circle into a heavy, an RTS player
box-selecting a battalion, and a 2D run-and-gun pilot holding eight-way on a
d-pad — all of them share **one pawn, one Ability System Component, and one
input subsystem**, and they differ only in which **Input Mapping Context** is
currently on top of the stack. That is the entire job of V4's input layer: take
Unreal's **Enhanced Input**, partition it cleanly into seven per-cell contexts,
and route the resulting actions either into the Gameplay Ability System through
a single `GA.Activation.Tag.*` namespace, into a fighting-game-style motion
recognizer for the ARPG cell, or into a planning cursor for the top-down cells.

Unlike V2 — whose input pipeline was a rollback-deterministic ring buffer
feeding a byte-exact replay encoder, because its entire combat core re-simulated
frames — V4's GAS stack is a conventional, replicated, server-authoritative one
(see [GAS Layout](./gas-layout.md)). V4's input pipeline reflects that: it is
real, working C++ built on Enhanced Input, but it is **not** a
frame-buffer/replay machine. It is a context-switching router, a per-cell data
manifest, a small directional motion parser, a cover-projection input modifier,
and a tac-map cursor. Where this page names a class or a path it names a real
artifact in `V4/ue/Source/V4Input` unless it says otherwise. This page is part
of the **Combat, Character & Input Systems** group; the section hub is
[../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md).

## What ships, honestly

The `V4Input` module is **real, working Unreal C++**, not a skeleton. It builds
against
`Core/CoreUObject/Engine/V4Core/EnhancedInput/GameplayAbilities/GameplayTags`
(`V4Input.Build.cs`) and ships five cooperating pieces, all exercised by a
single automation spec, `V4.Input.MotionParser.RecognizesArpgMotions`
(`V4/ue/Source/V4Tests/Private/V4InputTests/MotionSpec.cpp`):

- **`UV4InputRouter`** — a `ULocalPlayerSubsystem` that pushes/pops
  `UInputMappingContext`s onto an Enhanced Input stack and routes input actions
  to GAS by tag (`V4InputRouter.cpp`).
- **`UV4InputPipelineManifest`** — a `UDataAsset` that authors the full per-cell
  input table in code (48 input actions, 7 mapping contexts, 31 ability
  bindings, 6 cover-anchor graphs) and validates it
  (`V4InputPipelineManifest.cpp`, 404 lines).
- **`FMotionParser` / `UV4MotionParser`** — a directional motion recognizer over
  a 16-frame window (`V4MotionParser.cpp`, 276 lines).
- **`UV4CoverStickInputModifier`** — a real `UInputModifier` that projects stick
  input onto a cover tangent (`V4CoverStickInputModifier.cpp`).
- **`FPlanCursor` / `UV4TacMapCursorComponent`** — the top-down planning cursor
  with waypoint and vision-cone authoring (`V4PlanCursor.cpp`,
  `V4TacMapCursorComponent.cpp`).

Five honest qualifications, so the rest reads at face value:

1. **The architecture monolith's names drift from the code.**
   `V4_ARCHITECTURE.md` says the router lives in `V4Input::FInputRouter` and the
   parser in `V4Input::FMotionParser` "reused from V2." In the repo the router
   is the UObject subsystem `UV4InputRouter`, and the motion parser is a
   **fresh, smaller V4 reimplementation** — a clean-room cousin of V2's idea,
   not shared code. V2's parser recognized 22 motions (`EV2MotionInput`) with
   negative-edge, plink, tiger-knee and Korean-backdash logic; V4's
   `EV4MotionCommand` has 11 (`V4InputTypes.h:31`). Treat the "reused from V2"
   line as lineage, not a literal dependency.
2. **The mapping contexts are _specs_, not authored assets.** The manifest
   stores contexts as `FName`-keyed `FV4InputMappingContextSpec` rows (context
   name, cell, priority, action-name list) — `IMC_Tactical`, `IMC_Stealth`, …
   exist as **data**, not yet as `UInputMappingContext` `.uasset` binaries. The
   router consumes real `UInputMappingContext*` objects at runtime; the manifest
   describes which actions each context should carry and validates that table.
3. **`FPlanCursor` lives in `V4Input`, under namespace `V4Tactics`.** The
   monolith writes `V4Tactics::FPlanCursor` as if it were in a separate Tactics
   module; it is `V4Input/Public/V4PlanCursor.h`, namespaced `V4Tactics`. The
   namespace matches the prose; the module location does not.
4. **The cell-switch driver is a mechanism, not yet a shipping caller.** The
   router's push/pop is real and tested, but no shipping subsystem outside the
   test yet calls `PushInputMappingContext` on a cell change — that wiring
   (`EV4RulesetCell` flip → pop old IMC, push new) is the integration point the
   cell-state layer owns.
5. **There is no V2-style buffering or replay here.** No input ring buffer, no
   `IsInputBufferWindowSatisfied` GAS gate, no replay byte encoder exists
   anywhere in `V4Input`, `V4Gameplay`, or `V4Netcode`. "Buffering" in V4 means
   exactly two things: Enhanced Input's own trigger/hold state, and the motion
   parser's 16-frame sliding window. Nothing in this pipeline is frame-replayed.

## The shape of the pipeline

```mermaid
flowchart LR
  DEV[Device event] --> EI[EnhancedInput\nLocalPlayer subsystem]
  EI --> STK[Active IMC stack\nUV4InputRouter push/pop]
  STK --> IA[Input Action\nIA_Fire / IA_Move / IA_LightAttack ...]
  IA -->|Axis2D move in cover| MOD[UV4CoverStickInputModifier\nproject onto cover tangent]
  IA -->|ability input| ROUTE[UV4InputRouter::TriggerAbilityInput]
  ROUTE -->|TryActivateAbilitiesByTag| GAS[UV4GameplayAbilityBase\nIsAbilityCellCompatible gate]
  IA -->|ARPG stick history| MP[FMotionParser\n16-frame window]
  MP -->|MatchMotion| CMD[EV4MotionCommand\nQCF / HCF / charge]
  CMD --> GAS
  IA -->|Tactics cursor / waypoints| CUR[FPlanCursor\nwaypoints + RTST cones]
  MAN[UV4InputPipelineManifest] -. authors + validates .-> IA
  MAN -. ApplyAbilityBindings .-> ROUTE
```

Everything begins at Enhanced Input: a device event resolves, against the
currently applied mapping contexts, into a named **Input Action**. From there
the action takes one of four paths — the cover modifier reshapes a move axis,
the router fires a GAS ability by tag, the motion parser accumulates a
directional history for ARPG combos, or the plan cursor moves and drops
waypoints. The manifest is the off-to-the-side authority that declares every
action and binding and asserts the table is coherent.

## Enhanced Input and the mapping-context router

`UV4InputRouter` is a `ULocalPlayerSubsystem` (`V4InputRouter.h:13`) — one per
local player, the natural home for per-player context state. It keeps a
`MappingStack` of `FV4InputMappingStackEntry` rows, each a
`{UInputMappingContext*, Priority, LayerName}` triple (`V4InputTypes.h:61`).
Three calls drive it:

- `PushInputMappingContext(Context, Priority, LayerName)` appends an entry and
  rebuilds (`V4InputRouter.cpp:8`).
- `PopInputMappingContext(LayerName)` removes the **last** entry with that layer
  name — `FindLastByPredicate`, so push/pop nests correctly when the same layer
  is stacked twice — and rebuilds (`V4InputRouter.cpp:26`).
- `ClearInputMappingStack()` empties it.

The rebuild is the load-bearing part. `RebuildAppliedContexts` fetches the
`UEnhancedInputLocalPlayerSubsystem` from the owning `ULocalPlayer`, calls
`ClearAllMappings()`, then re-adds every stacked context at its stored priority
(`V4InputRouter.cpp:108`). This is a genuine Enhanced Input integration: the V4
stack is the source of truth, and Enhanced Input's own context set is re-derived
from it on every change. Priority is how the menu layer wins: in the manifest
the cell contexts sit at priority `100` while `IMC_UI` sits at `1000`
(`V4InputPipelineManifest.cpp:148`), so pushing the UI context on top of, say,
`IMC_Tactical` lets menu navigation shadow fire/reload without popping the
gameplay context. Switching cells mid-session is therefore "pop the old cell's
`LayerName`, push the new cell's `UInputMappingContext`" — the router supplies
that mechanism; the cell-state layer (`EV4RulesetCell`,
`V4Core/Public/V4CellState.h:7`) supplies the trigger.

## From input action to ability: the `GA.Activation.Tag.*` seam

GAS is reached without any per-action C++ wiring. The router holds a
`TMap<FName, FGameplayTag>` of activation tags by input-action name.
`RegisterAbilityInputBinding` populates it (rejecting `None` names and invalid
tags), and `TriggerAbilityInput(ActionName, ASC)` looks the tag up and calls
`AbilitySystemComponent->TryActivateAbilitiesByTag(...)` with a one-tag
container (`V4InputRouter.cpp:64`). So `IA_Fire` does not know about `GA_Fire`;
it knows the tag `GA.Activation.Tag.Fire`, and GAS resolves which granted
ability answers it. The ability then self-gates on cell:
`UV4GameplayAbilityBase::ActivateAbility` fails with reason `wrong-cell` if
`IsAbilityCellCompatible` is false (`V4GameplayAbilities.cpp:32`), which is what
keeps a Tactical `IA_Fire` from firing an ARPG verb even if a stray tag leaks
through. The bindings themselves are authored in the manifest as
`FV4InputAbilityBindingSpec` rows —
`{IA_Fire → GA_Fire → GA.Activation.Tag.Fire}` — and
`ApplyAbilityBindings(Router)` pushes all 31 into the router in one call
(`V4InputPipelineManifest.cpp:393`). The verbs those tags reach are detailed in
[GAS Layout](./gas-layout.md).

## The per-cell input manifest

`UV4InputPipelineManifest::LoadDefaultInputPipeline` is the single source of
truth for "what can the player press, in which cell, and what does it do." It
populates four arrays in code (`V4InputPipelineManifest.cpp:91`):

- **48 input actions** (`FV4InputActionSpec`), each carrying a value type
  (`Boolean`/`Axis1D`/`Axis2D`), the set of `EV4RulesetCell`s it belongs to, an
  optional `GA.Activation.Tag.*`, and the `bUsesCoverStickModifier` flag.
  `IA_Move` is shared across Tactical/Stealth/ARPG and is the one action flagged
  for cover projection; cell-specific verbs like `IA_BreachStack` (Tactical),
  `IA_BodyDrag` (Stealth), `IA_Parry` (ARPG), `IA_TrainUnit` (RTS), and
  `IA_Jump2D` (Arcade) are scoped to their one cell.
- **7 mapping contexts** — `IMC_Tactical`, `IMC_Stealth`, `IMC_Tactics`,
  `IMC_ARPG`, `IMC_RTS`, `IMC_Arcade` (priority 100) and `IMC_UI`
  (priority 1000) — each listing the action names it activates.
- **31 ability bindings** mapping ability-input actions to `GA_*` ids and
  activation tags.
- **6 cover-anchor graphs**, one per launch map, each built from named anchors
  (`EntryStack`/`FlankLeft`/`FlankRight`) with location, surface normal, width,
  and left/right peek flags, plus edges describing connectivity
  (`V4InputPipelineManifest.cpp:73`).

The value of a manifest is its validator. `ValidateInputPipeline(OutIssues)` is
a real gate, not a smoke test (`V4InputPipelineManifest.cpp:199`). It asserts:
all seven required IMCs are present; every action has a name; every context
names only actions that exist and is non-empty; **`IA_Move` is `Axis2D` and
carries the cover-stick modifier** (`:261`); every `bGameplayAbilityInput`
action's tag begins with `GA.Activation.Tag.` and has a matching binding whose
`AbilityId` begins with `GA_`; and every cover graph has ≥2 named anchors, ≥1
edge, positive anchor widths, and edges that reference only declared anchors.
The spec runs the validator and fails the test on any issue, so the table cannot
drift silently.

## The motion parser (ARPG input scheme)

Only the ARPG cell uses fighting-game motions, and `FMotionParser` is the
recognizer (`V4MotionParser.h:10`). It is a stateless-by-config value type with
three tunables: `WindowFrames = 16`, `DirectionDeadZone = 0.35`,
`ChargeMinFrames = 10` (`V4MotionParser.h:20`). The UObject wrapper
`UV4MotionParser` exposes the same surface to Blueprint and copies the editable
settings into the native parser before each query (`V4MotionParser.cpp:262`).

**The sample model.** `AddDirection(Direction, Frame)` ignores `Neutral`,
**coalesces a repeat of the current direction by just updating its frame** (so a
held direction is one sample, not many), otherwise appends a new
`FV4MotionSample{Direction, Frame, StartFrame}`, then prunes any sample older
than `WindowFrames` (`V4MotionParser.cpp:17`). `StartFrame` is preserved across
coalescing, which is what makes charge timing measurable. The raw-stick path
`AddDirectionalInput(RawInput, Frame)` first quantizes: vectors shorter than the
deadzone are `Neutral`, otherwise the X/Y signs against the deadzone resolve all
eight compass directions, with diagonals taking precedence
(`V4MotionParser.cpp:92`). The spec checks `FVector2D(0.8, -0.8) → DownForward`.

**Matching.** `MatchMotion(Command, CurrentFrame)` switches on
`EV4MotionCommand` and dispatches to one of three matchers
(`V4MotionParser.cpp:44`):

- **Sequences** (`MatchSequence`) walk the sample history newest-to-oldest,
  consuming the required direction list in reverse and ignoring samples older
  than the window (`:144`). `QuarterCircleForward` is
  `{Down, DownForward, Forward}`, `HalfCircleForward` is the five-step
  `{Back, DownBack, Down, DownForward, Forward}`, and `FullCircle` accepts
  either rotation direction via two sequences OR-ed together (`:64`). Because it
  scans for the directions in order but not contiguously, intermediate noise
  inside the window is tolerated — sloppy diagonals still read.
- **Charges** (`MatchCharge`) find a release sample in the target direction
  within the window, then look further back for a hold sample in the charge
  direction whose `StartFrame` is at least `ChargeMinFrames` earlier (`:173`).
  `ChargeBackForward` and `ChargeDownUp` are the two classic charge inputs.
- **Motion-charges** (`MatchMotionCharge`) require both a full directional
  sequence **and** a held transition between two adjacent samples separated by
  ≥`ChargeMinFrames` (`:199`) — the Mishima-style "hold then roll" command.

**What it's tested against.** `MotionSpec.cpp` builds real frame histories and
asserts: a `{Down@1, DownForward@4, Forward@8}` reads as both QCF and
partial-circle at frame 8 but **expires by frame 40** (window enforcement); a
five-step input reads as half-circle-forward; a back-hold-then-forward with
`ChargeMinFrames = 6` reads as a back-forward charge; a longer
hold-into-quarter-circle reads as a motion-charge; an eight-direction sweep
reads as a full circle; and the native `FMotionParser` recognizes partial
circles directly. These would fail against a stub — they pin specific frames and
specific commands. The button that confirms the motion is **not** matched here:
it arrives as its own ability-input action (`IA_LightAttack`, `IA_HeavyAttack`),
and combat correlates motion + button. The parser only answers "was this shape
drawn within 16 frames."

## Cover-stick and tac-map cursor

Two per-cell schemes round out the module. **`UV4CoverStickInputModifier`** is a
real `UInputModifier` whose `ModifyRaw_Implementation` reshapes a 2D stick value
by projecting it onto a `CoverTangent`: `ProjectInputOntoCover` normalizes the
tangent (falling back to `(1,0)` if degenerate) and returns
`tangent * dot(input, tangent)` (`V4CoverStickInputModifier.cpp:3`). That is the
"left-stick scoots along cover" feel — any push gets flattened onto the cover
wall's direction. The spec confirms `(1,1)` projected onto `(1,0)` yields
`(1,0)`. The anchor graph it slides along is the manifest's cover data; the
modifier consumes a tangent, the graph supplies anchors and edges, and the
traversal that walks between them is the cover component's job in the per-cell
layer, not this module's.

**`FPlanCursor`** (and its `UV4TacMapCursorComponent` wrapper) is the
Tactics/RTS cursor (`V4PlanCursor.h:15`). `MoveCursor(Input, DeltaSeconds)`
clamps the stick to unit length, advances at `CursorSpeed`, and clamps to
`WorldBounds` (`V4PlanCursor.cpp:5`). Beyond moving, it does two authoring jobs
the architecture calls out: it records **plan waypoints** (`AddPlanWaypoint`
snapshots the cursor location — the Raven Shield planning step) and it authors
**vision cones** for the RTST cone tool via `BeginConeAuthoring` /
`UpdateConeAuthoring` (origin → aim direction, range, half-angle clamped to
`[1,179]°`) / `CommitConeAuthoring` (`V4PlanCursor.cpp:41`). The spec drives the
shared cursor through a commit and asserts one stored waypoint and one authored
cone, which is exactly the "shared by the planning step and the cone-authoring
tool" claim made concrete.

## Failure modes and edge cases

- **Null and empty.** `PushInputMappingContext(nullptr, …)` is a no-op;
  `RebuildAppliedContexts` bails if there is no Enhanced Input subsystem (e.g.
  no local player yet), so the stack stays consistent and re-applies later.
- **Held vs. tapped.** Because `AddDirection` coalesces repeats into one sample,
  a held direction cannot inflate into a sequence, and charge timing reads off
  the preserved `StartFrame` rather than sample count.
- **Window expiry.** Any sample older than `WindowFrames` is pruned on insert
  and skipped on match, so a motion that takes too long simply never completes —
  the tested "QCF expires by frame 40" behavior.
- **Wrong-cell leakage.** Even if an activation tag reaches the wrong cell's
  ability, the GAS `IsAbilityCellCompatible` gate rejects it with `wrong-cell`,
  so a mis-stacked context cannot fire a foreign verb.
- **Manifest incoherence.** A context that names a missing action, an ability
  action with no binding, a non-`Axis2D` `IA_Move`, or a cover graph with <2
  anchors all fail `ValidateInputPipeline`, and the automation spec turns those
  issues into test failures.

## Related

- [GAS Layout](./gas-layout.md) — the `GA.Activation.Tag.*` verbs these actions
  fire and the cell-compatibility gate that scopes them
- [Animation Pipeline](./animation-pipeline.md) — the montages, combo machines,
  and dodge/parry frame windows the ability activations drive
- [Per-Cell Deep-Dives](./per-cell-deep-dives.md) — the cover-traversal,
  planning, and combo components the cover graph, plan cursor, and motion parser
  feed
- The section hub: [../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md)
