Concept: The Settings-Provider Bridge
The four input modifiers need per-player values (deadzone, sensitivity, inversion). Rather than importing a settings class, they read a small interface - ICrimsonInputSettingsProvider - that your ULocalPlayer implements. Input stays ignorant of where settings come from; settings stay ignorant of input.
How a modifier finds the provider
Every modifier resolves the provider the same way, at evaluation time:
// UEnhancedPlayerInput -> owning APlayerController -> its ULocalPlayer -> the interfaceAPlayerController* PC = Cast<APlayerController>(PlayerInput->GetOuter());ICrimsonInputSettingsProvider* Provider = PC ? Cast<ICrimsonInputSettingsProvider>(PC->GetLocalPlayer()) : nullptr;
If the cast fails (no provider), each modifier returns the input unchanged - so a project that never implements the interface simply gets vanilla behavior, never a crash.
C++ virtuals vs K2_ events
The interface exposes two parallel surfaces. The C++ virtuals (GetGamepadLookStickDeadZone, GetInvertVerticalAxis, ...) are what the modifiers call; their default implementations forward to the matching K2_ BlueprintNativeEvent. Override the virtuals in a C++ local player for zero overhead, or the K2_ events in a Blueprint local player - both reach the same modifier.
| Modifier reads | Provider method |
|---|---|
| Deadzone (move / look) | GetGamepadMoveStickDeadZone / GetGamepadLookStickDeadZone |
| Sensitivity (normal / ADS) | GetGamepadLookSensitivityPreset / GetGamepadTargetingSensitivityPreset |
| Invert axes | GetInvertVerticalAxis / GetInvertHorizontalAxis |
| Named scalar properties | GetSharedSettingsObject (C++ only; null by default) |
UCrimsonSettingBasedScalar reads named float/double properties off GetSharedSettingsObject() by reflection (caching the FProperty*). That method returns null by default, so the scalar modifier is inert until you return your settings object from it.UCrimsonCoreLocalPlayer (the project's local player) implements this interface and forwards each value to UCrimsonSettingsShared in CrimsonSettings - so the modifiers are driven by the player's saved settings out of the box. Standalone (CrimsonInput alone) it is your job: UCrimsonCommonLocalPlayer does not implement the interface (CrimsonCommon may not depend on CrimsonInput or CrimsonSettings), so implement it on your own local player and point it at your settings. The split keeps CrimsonInput free of any settings-backend dependency.