Concept: Storage Model
There are two persistent backends plus one generic store. Choosing the right one is about scope - is a value tied to this machine, or to the player's account?
| Backend | Base | Scope | Use for |
|---|---|---|---|
UCrimsonSettingsLocal | UGameUserSettings (GameUserSettings.ini) | This machine | Hardware-coupled state: resolution, scalability, gamma, frame pacing. Reached via UCrimsonSettingsLocal::Get(). |
UCrimsonSettingsShared | ULocalPlayerSaveGame (save slot) | This player (follows the account) | Player taste: audio, sensitivity, camera options, accessibility. Loaded via LoadOrCreateSettings(LocalPlayer). |
| Keyed store | A TMap on UCrimsonSettingsShared | This player | The generic Key -> value bag that Persist To Shared Store writes to - no typed field needed. |
The keyed store + subsystem
UCrimsonSettingsShared holds a generic Key -> value map and typed accessors (Get/Set Float/Int/Bool/String Value, Has Keyed Value). It is stored as strings (scalar = number, discrete = index) - the same format the profile system uses - so any value round-trips. Writing a key marks the save dirty and broadcasts OnSettingChanged.
UCrimsonSharedSettingsSubsystem (a ULocalPlayerSubsystem) owns the per-player UCrimsonSettingsShared at runtime: it loads it, exposes it via GetSharedSettings(), and flushes SaveSettings() on a short debounce whenever a value changes. Always reach the shared object through the subsystem - there is no global singleton.
StorageKey and reads it back by name. That is the mechanism behind How-To: Expose Another Plugin's Settings.UCrimsonSettingsShared can be made available server-side for authority checks where a preference affects gameplay, but the screen and the keyed store run on the owning client. Don't drive authoritative gameplay from a client's preference value.