# V2 Hold-To-Toggle Conversion

This runbook covers `TODOS.phase-72.72.13.2.2`: allowing any hold action to be
set to toggle, including hold-to-aim becoming toggle-aim and hold-to-sprint
becoming toggle-sprint.

## Runtime Surface

Hold-to-toggle conversion extends the V2Input accessibility profile model:

- profile: `FV2HoldToToggleConversionProfile`
- action rule: `FV2HoldToToggleActionRule`
- player preference: `FV2HoldToTogglePreference`
- resolved action: `FV2ResolvedHoldToToggleAction`
- source library: `UV2InputBlueprintLibrary`
- config asset: `UV2InputConfigAsset`
- UI config: `FV2AccessibilityMotorAssistConfig`
- persistence snapshot: `FV2PlayerSettingsSnapshot`
- save game: `UV2SaveGame`
- persistence subsystem: `UV2PersistenceSubsystem`
- contract:
  `V2/ue/Content/V2/Input/Accessibility/HoldToToggleConversion_V2_Contract.json`
- focused checker: `V2/ue/Tools/check-v2-hold-to-toggle-conversion.py`

## Behavior

`FV2HoldToToggleActionRule` defines a hold action, its toggle alternative, and
the mapping slot or settings row that owns the action. The default rules include
aim, sprint, block, grapple, Blazin, submission grid, pin press, and run. Other
hold actions can be added by data using the same rule struct.

`FV2HoldToToggleConversionProfile` stores player-authored preferences per
action. Resolution returns the hold input action by default and switches to the
toggle input action only when a validated preference requests toggle mode.

## Validation

Preferences are applied to a candidate profile first. A candidate is rejected if
it targets an unknown action, a non-hold action, a duplicated action, or a rule
without a toggle alternative. Rejected preferences do not replace the last valid
profile.

## Persistence

`FV2PlayerSettingsSnapshot` stores `HoldToToggleProfile` with startup flags.
`UV2SaveGame::SaveHoldToToggleProfile` persists a validated profile per player,
`GetStartupHoldToToggleProfile` resolves it on startup, and
`MarkHoldToToggleAppliedOnStartup` records the before-gameplay apply time.
`ValidateSave` rejects profiles whose account id does not match the save header.

## Verification

Run these checks after changing hold-to-toggle conversion:

```bash
python3 V2/ue/Tools/check-v2-hold-to-toggle-conversion.py
python3 V2/ue/Tools/check-v2-input-mapping-contexts.py
python3 V2/ue/Tools/check-v2-profile-save-game.py
python3 V2/ue/Tools/check-v2-persistence-module.py
python3 V2/ue/Tools/check-v2-accessibility-feature-set.py
python3 V2/ue/Tools/check-v2-ci-workflow.py
python3 V2/tools/validate-v2-docs.py
python3 -m py_compile V2/ue/Tools/check-v2-hold-to-toggle-conversion.py V2/ue/Tools/check-v2-ci-workflow.py
python3 -m json.tool V2/ue/Content/V2/Input/Accessibility/HoldToToggleConversion_V2_Contract.json
```

Automation coverage:

- `V2.Input.Module`
- `V2.Persistence.Module`
- `V2.Persistence.ProfileSave.AssetContract`
- `V2.UI.Module`

## Horde Gates

- `hold-to-toggle-source`
- `hold-to-toggle-any-hold-action`
- `hold-to-toggle-aim-sprint`
- `hold-to-toggle-reject-non-hold`
- `hold-to-toggle-per-player-persistence`
- `hold-to-toggle-startup-apply`
- `hold-to-toggle-ci-wired`
