-
Notifications
You must be signed in to change notification settings - Fork 6
Settings and Serialization
Developer reference for PadForge's settings persistence layer: XML file format, data models, SettingsManager, and the serialization pipeline.
Source files:
-
PadForge.App/Services/SettingsService.cs-- XML load/save, serialization DTOs -
PadForge.App/Common/SettingsManager.cs-- static thread-safe collections and slot management -
PadForge.Engine/Data/PadSetting.cs-- mapping configuration model (all properties) -
PadForge.Engine/Data/UserDevice.cs-- physical device record -
PadForge.Engine/Data/UserSetting.cs-- device-to-slot linkage
- PadForge.xml File Format
- UserDevice
- UserSetting
- PadSetting
- AppSettingsData
- MacroData and ActionData
- ProfileData
- SettingsManager
- Serialization Pipeline
- Cross-Layout Mapping Translation
- Backward Compatibility
The settings file is an XML document with SettingsFileData as the root element (serialized as <PadForgeSettings>). The file lives next to the executable.
Search order (in SettingsService.FindSettingsFile()):
-
PadForge.xml-- preferred for new installs -
Settings.xml-- generic fallback for legacy installations - If neither exists, creates
PadForge.xmlin the application directory
[XmlRoot("PadForgeSettings")]
public class SettingsFileData
{
[XmlArray("Devices")]
[XmlArrayItem("Device")]
public UserDevice[] Devices { get; set; }
[XmlArray("UserSettings")]
[XmlArrayItem("Setting")]
public UserSetting[] Settings { get; set; }
[XmlArray("PadSettings")]
[XmlArrayItem("PadSetting")]
public PadSetting[] PadSettings { get; set; }
[XmlElement("AppSettings")]
public AppSettingsData AppSettings { get; set; }
[XmlArray("Macros")]
[XmlArrayItem("Macro")]
public MacroData[] Macros { get; set; }
[XmlArray("Profiles")]
[XmlArrayItem("Profile")]
public ProfileData[] Profiles { get; set; }
}<PadForgeSettings>
<!-- Physical input devices (persisted across sessions) -->
<Devices>
<Device>
<InstanceGuid>00000000-0000-0000-0000-000000000000</InstanceGuid>
<InstanceName>Xbox Controller</InstanceName>
<ProductGuid>...</ProductGuid>
<ProductName>Xbox One Controller</ProductName>
<VendorId>1118</VendorId>
<ProdId>654</ProdId>
<DevRevision>0</DevRevision>
<DevicePath>\\?\hid#vid_045e&pid_028e...</DevicePath>
<SerialNumber></SerialNumber>
<CapAxeCount>6</CapAxeCount>
<CapButtonCount>11</CapButtonCount>
<RawButtonCount>11</RawButtonCount>
<CapPovCount>1</CapPovCount>
<CapType>4</CapType>
<CapSubType>0</CapSubType>
<CapFlags>0</CapFlags>
<HasGyro>false</HasGyro>
<HasAccel>false</HasAccel>
<DateCreated>2025-01-15T10:30:00</DateCreated>
<DateUpdated>2025-01-15T10:30:00</DateUpdated>
<IsEnabled>true</IsEnabled>
<IsHidden>false</IsHidden>
<DisplayName></DisplayName>
<HidHideEnabled>false</HidHideEnabled>
<ConsumeInputEnabled>false</ConsumeInputEnabled>
<ForceRawJoystickMode>false</ForceRawJoystickMode>
<HidHideInstanceIds />
</Device>
</Devices>
<!-- Device-to-slot assignments (links device to a virtual controller slot) -->
<UserSettings>
<Setting>
<InstanceGuid>00000000-0000-0000-0000-000000000000</InstanceGuid>
<InstanceName>Xbox Controller</InstanceName>
<ProductGuid>...</ProductGuid>
<ProductName>Xbox One Controller</ProductName>
<MapTo>0</MapTo>
<PadSettingChecksum>A1B2C3D4</PadSettingChecksum>
<IsEnabled>true</IsEnabled>
<DateCreated>2025-01-15T10:30:00</DateCreated>
<DateUpdated>2025-01-15T10:30:00</DateUpdated>
</Setting>
</UserSettings>
<!-- Mapping configurations (deduplicated by checksum) -->
<PadSettings>
<PadSetting>
<PadSettingChecksum>A1B2C3D4</PadSettingChecksum>
<!-- Button mappings -->
<ButtonA>Button 0</ButtonA>
<ButtonB>Button 1</ButtonB>
<ButtonX>Button 2</ButtonX>
<ButtonY>Button 3</ButtonY>
<LeftShoulder>Button 4</LeftShoulder>
<RightShoulder>Button 5</RightShoulder>
<ButtonBack>Button 6</ButtonBack>
<ButtonStart>Button 7</ButtonStart>
<ButtonGuide>Button 10</ButtonGuide>
<LeftThumbButton>Button 8</LeftThumbButton>
<RightThumbButton>Button 9</RightThumbButton>
<!-- D-Pad -->
<DPad></DPad>
<DPadUp>POV 0 Up</DPadUp>
<DPadDown>POV 0 Down</DPadDown>
<DPadLeft>POV 0 Left</DPadLeft>
<DPadRight>POV 0 Right</DPadRight>
<!-- Triggers -->
<LeftTrigger>Axis 2</LeftTrigger>
<RightTrigger>Axis 5</RightTrigger>
<LeftTriggerDeadZone>0</LeftTriggerDeadZone>
<RightTriggerDeadZone>0</RightTriggerDeadZone>
<LeftTriggerAntiDeadZone>0</LeftTriggerAntiDeadZone>
<RightTriggerAntiDeadZone>0</RightTriggerAntiDeadZone>
<LeftTriggerMaxRange>100</LeftTriggerMaxRange>
<RightTriggerMaxRange>100</RightTriggerMaxRange>
<!-- Thumbstick axes -->
<LeftThumbAxisX>Axis 0</LeftThumbAxisX>
<LeftThumbAxisY>Axis 1</LeftThumbAxisY>
<RightThumbAxisX>Axis 3</RightThumbAxisX>
<RightThumbAxisY>Axis 4</RightThumbAxisY>
<LeftThumbAxisXNeg></LeftThumbAxisXNeg>
<LeftThumbAxisYNeg></LeftThumbAxisYNeg>
<RightThumbAxisXNeg></RightThumbAxisXNeg>
<RightThumbAxisYNeg></RightThumbAxisYNeg>
<!-- Dead zones -->
<LeftThumbDeadZoneX>0</LeftThumbDeadZoneX>
<LeftThumbDeadZoneY>0</LeftThumbDeadZoneY>
<RightThumbDeadZoneX>0</RightThumbDeadZoneX>
<RightThumbDeadZoneY>0</RightThumbDeadZoneY>
<LeftThumbDeadZoneShape>2</LeftThumbDeadZoneShape>
<RightThumbDeadZoneShape>2</RightThumbDeadZoneShape>
<LeftThumbAntiDeadZone>0</LeftThumbAntiDeadZone>
<RightThumbAntiDeadZone>0</RightThumbAntiDeadZone>
<LeftThumbAntiDeadZoneX>0</LeftThumbAntiDeadZoneX>
<LeftThumbAntiDeadZoneY>0</LeftThumbAntiDeadZoneY>
<RightThumbAntiDeadZoneX>0</RightThumbAntiDeadZoneX>
<RightThumbAntiDeadZoneY>0</RightThumbAntiDeadZoneY>
<LeftThumbLinear>0</LeftThumbLinear>
<RightThumbLinear>0</RightThumbLinear>
<!-- Sensitivity curves -->
<LeftThumbSensitivityCurveX>0</LeftThumbSensitivityCurveX>
<LeftThumbSensitivityCurveY>0</LeftThumbSensitivityCurveY>
<RightThumbSensitivityCurveX>0</RightThumbSensitivityCurveX>
<RightThumbSensitivityCurveY>0</RightThumbSensitivityCurveY>
<LeftTriggerSensitivityCurve>0</LeftTriggerSensitivityCurve>
<RightTriggerSensitivityCurve>0</RightTriggerSensitivityCurve>
<!-- Max range -->
<LeftThumbMaxRangeX>100</LeftThumbMaxRangeX>
<LeftThumbMaxRangeY>100</LeftThumbMaxRangeY>
<RightThumbMaxRangeX>100</RightThumbMaxRangeX>
<RightThumbMaxRangeY>100</RightThumbMaxRangeY>
<!-- Independent per-direction max range (null = symmetric) -->
<LeftThumbMaxRangeXNeg>100</LeftThumbMaxRangeXNeg>
<LeftThumbMaxRangeYNeg>100</LeftThumbMaxRangeYNeg>
<RightThumbMaxRangeXNeg>100</RightThumbMaxRangeXNeg>
<RightThumbMaxRangeYNeg>100</RightThumbMaxRangeYNeg>
<!-- Center offset calibration -->
<LeftThumbCenterOffsetX>0</LeftThumbCenterOffsetX>
<LeftThumbCenterOffsetY>0</LeftThumbCenterOffsetY>
<RightThumbCenterOffsetX>0</RightThumbCenterOffsetX>
<RightThumbCenterOffsetY>0</RightThumbCenterOffsetY>
<!-- Force feedback -->
<ForceType>1</ForceType>
<ForceOverall>100</ForceOverall>
<ForceSwapMotor>0</ForceSwapMotor>
<LeftMotorStrength>100</LeftMotorStrength>
<RightMotorStrength>100</RightMotorStrength>
<!-- Audio bass rumble -->
<AudioRumbleEnabled>0</AudioRumbleEnabled>
<AudioRumbleSensitivity>4</AudioRumbleSensitivity>
<AudioRumbleCutoffHz>80</AudioRumbleCutoffHz>
<AudioRumbleLeftMotor>100</AudioRumbleLeftMotor>
<AudioRumbleRightMotor>100</AudioRumbleRightMotor>
<!-- Axis inversion -->
<LeftThumbAxisXInvert>0</LeftThumbAxisXInvert>
<LeftThumbAxisYInvert>0</LeftThumbAxisYInvert>
<RightThumbAxisXInvert>0</RightThumbAxisXInvert>
<RightThumbAxisYInvert>0</RightThumbAxisYInvert>
<!-- Other -->
<AxisToButtonThreshold>50</AxisToButtonThreshold>
<GameFileName></GameFileName>
<!-- Dictionary-based mappings (only present when non-empty) -->
<VJoyMappings>
<Map Key="VJoyAxis0" Value="Axis 0" />
<Map Key="VJoyBtn0" Value="Button 0" />
</VJoyMappings>
<MidiMappings>
<Map Key="MidiCC0" Value="Axis 0" />
<Map Key="MidiNote0" Value="Button 0" />
</MidiMappings>
<KbmMappings>
<Map Key="KbmKey41" Value="Button 0" />
<Map Key="KbmMouseX" Value="Axis 0" />
</KbmMappings>
</PadSetting>
</PadSettings>
<!-- Application-level settings (single element) -->
<AppSettings>
<AutoStartEngine>true</AutoStartEngine>
<MinimizeToTray>false</MinimizeToTray>
<StartMinimized>false</StartMinimized>
<StartAtLogin>false</StartAtLogin>
<EnablePollingOnFocusLoss>true</EnablePollingOnFocusLoss>
<PollingRateMs>1</PollingRateMs>
<ThemeIndex>0</ThemeIndex>
<Language></Language>
<EnableAutoProfileSwitching>false</EnableAutoProfileSwitching>
<ActiveProfileId />
<SlotControllerTypes>
<Type>0</Type> <!-- VirtualControllerType enum: 0=Xbox360, 1=DS4, 2=VJoy, 3=Midi, 4=KeyboardMouse -->
<Type>1</Type>
</SlotControllerTypes>
<SlotCreated>
<Created>true</Created>
<Created>true</Created>
<Created>false</Created>
<!-- ... up to 16 entries -->
</SlotCreated>
<SlotEnabled>
<Enabled>true</Enabled>
<Enabled>true</Enabled>
<Enabled>true</Enabled>
</SlotEnabled>
<EnableDsuMotionServer>false</EnableDsuMotionServer>
<DsuMotionServerPort>26760</DsuMotionServerPort>
<EnableWebController>false</EnableWebController>
<WebControllerPort>8080</WebControllerPort>
<Use2DControllerView>false</Use2DControllerView>
<EnableInputHiding>true</EnableInputHiding>
<HidHideWhitelistPaths>
<Path>C:\Games\emulator.exe</Path>
</HidHideWhitelistPaths>
<VJoyConfigs>
<Config SlotIndex="2">
<Preset>Custom</Preset>
<ThumbstickCount>2</ThumbstickCount>
<TriggerCount>2</TriggerCount>
<PovCount>1</PovCount>
<ButtonCount>11</ButtonCount>
</Config>
</VJoyConfigs>
<MidiConfigs>
<Config SlotIndex="3" Channel="1" CcCount="6" StartCc="1" NoteCount="11" StartNote="60" Velocity="127" />
</MidiConfigs>
<!-- Snapshot of default profile state when a named profile is active (null when default is active) -->
<DefaultProfileSnapshot>
<!-- Same structure as a Profile element -->
</DefaultProfileSnapshot>
</AppSettings>
<!-- Macros (per-slot, PadIndex attribute identifies the slot) -->
<Macros>
<Macro PadIndex="0">
<Name>Turbo A</Name>
<IsEnabled>true</IsEnabled>
<TriggerButtons>4096</TriggerButtons>
<TriggerSource>OutputController</TriggerSource>
<TriggerMode>Hold</TriggerMode>
<ConsumeTriggerButtons>true</ConsumeTriggerButtons>
<RepeatMode>WhileHeld</RepeatMode>
<RepeatCount>1</RepeatCount>
<RepeatDelayMs>50</RepeatDelayMs>
<TriggerAxisThreshold>50</TriggerAxisThreshold>
<Actions>
<Action>
<Type>Button</Type>
<ButtonFlags>4096</ButtonFlags>
<DurationMs>50</DurationMs>
</Action>
</Actions>
</Macro>
</Macros>
<!-- Per-application profiles (self-contained snapshots) -->
<Profiles>
<Profile Id="abc123def456">
<Name>Game Profile</Name>
<ExecutableNames>C:\Games\game.exe|D:\Other\game2.exe</ExecutableNames>
<Entries>
<Entry>
<InstanceGuid>00000000-0000-0000-0000-000000000000</InstanceGuid>
<ProductGuid>...</ProductGuid>
<MapTo>0</MapTo>
<PadSettingChecksum>A1B2C3D4</PadSettingChecksum>
</Entry>
</Entries>
<ProfilePadSettings>
<PadSetting>
<!-- Full PadSetting structure as above -->
</PadSetting>
</ProfilePadSettings>
<ProfileMacros>
<Macro PadIndex="0"><!-- ... --></Macro>
</ProfileMacros>
<ProfileSlotCreated>
<Created>true</Created>
<Created>false</Created>
</ProfileSlotCreated>
<ProfileSlotEnabled>
<Enabled>true</Enabled>
<Enabled>true</Enabled>
</ProfileSlotEnabled>
<ProfileSlotControllerTypes>
<Type>0</Type>
<Type>1</Type>
</ProfileSlotControllerTypes>
<ProfileVJoyConfigs>
<VJoyConfig SlotIndex="2"><!-- ... --></VJoyConfig>
</ProfileVJoyConfigs>
<ProfileMidiConfigs>
<MidiConfig SlotIndex="3" Channel="1" CcCount="6" StartCc="1" NoteCount="11" StartNote="60" Velocity="127" />
</ProfileMidiConfigs>
<EnableDsuMotionServer>false</EnableDsuMotionServer>
<DsuMotionServerPort>26760</DsuMotionServerPort>
<EnableWebController>false</EnableWebController>
<WebControllerPort>8080</WebControllerPort>
</Profile>
</Profiles>
</PadForgeSettings>-
PadSettings are deduplicated by checksum. Multiple UserSettings may reference the same
PadSettingChecksum; only one copy of the PadSetting is serialized. This keeps the file small when multiple devices share identical mappings. -
All PadSetting mapping/numeric properties are string-typed. This maintains XML serialization consistency with the original x360ce format and allows empty strings to represent "unmapped."
-
Profiles are self-contained snapshots. Each
ProfileDatastores its ownPadSettings[],Entries[], slot topology, vJoy/MIDI configs, and server settings independently from the root-level data. Switching profiles replaces the runtime state wholesale. -
Default profile snapshot preservation. When a named profile is active,
AppSettings.DefaultProfileSnapshotstores the default profile's complete state so it can be restored on restart without data loss. The root-levelSlotCreated/SlotEnabled/SlotControllerTypesalways represent the default profile's state; the active named profile's topology overwrites them at load time.
File: PadForge.Engine/Data/UserDevice.cs
Namespace: PadForge.Engine.Data
Implements: INotifyPropertyChanged
Represents a single physical input device. Contains both serializable (persisted to XML) properties and runtime-only fields used during the input pipeline.
| Property | Type | XML Element | Description |
|---|---|---|---|
InstanceGuid |
Guid |
<InstanceGuid> |
Deterministic GUID derived from the device's file system path. Unique per USB port + device combination. |
InstanceName |
string |
<InstanceName> |
Human-readable instance name (e.g., "Xbox Controller"). May differ from ProductName. |
ProductGuid |
Guid |
<ProductGuid> |
Product GUID in PIDVID format. Used for fallback matching when instance GUIDs change (e.g., device plugged into a different USB port). |
ProductName |
string |
<ProductName> |
Human-readable product name. |
VendorId |
ushort |
<VendorId> |
USB Vendor ID (e.g., 1118 for Microsoft). |
ProdId |
ushort |
<ProdId> |
USB Product ID. |
DevRevision |
ushort |
<DevRevision> |
USB Product Version / Revision. Only populated for joystick devices (from SdlDeviceWrapper.ProductVersion). |
DevicePath |
string |
<DevicePath> |
File system device path. Used for InstanceGuid generation. |
SerialNumber |
string |
<SerialNumber> |
Device serial number (e.g., Bluetooth MAC address). Empty if unavailable. |
| Property | Type | XML Element | Description |
|---|---|---|---|
CapAxeCount |
int |
<CapAxeCount> |
Number of axes on the device. |
CapButtonCount |
int |
<CapButtonCount> |
Number of buttons (gamepad-mapped count for gamepad devices, which is always 11). |
RawButtonCount |
int |
<RawButtonCount> |
Total raw joystick buttons before gamepad remapping. For gamepad devices, this is higher than CapButtonCount -- it exposes extra native buttons like DualSense touchpad click or mic button. For non-gamepad devices, equals CapButtonCount. |
CapPovCount |
int |
<CapPovCount> |
Number of POV hat switches. |
CapType |
int |
<CapType> |
Device type constant from InputDeviceType (0=Unknown, 1=Mouse, 2=Keyboard, 4=Gamepad, 5=Joystick). |
CapSubType |
int |
<CapSubType> |
Device subtype (not available from SDL, always 0). |
CapFlags |
int |
<CapFlags> |
Capability flags (not available from SDL, always 0). |
HasGyro |
bool |
<HasGyro> |
Whether the device has a gyroscope sensor (e.g., DualSense, Switch Pro Controller). |
HasAccel |
bool |
<HasAccel> |
Whether the device has an accelerometer sensor. |
| Property | Type | XML Element | Description |
|---|---|---|---|
DateCreated |
DateTime |
<DateCreated> |
When this device record was first created (constructor sets to DateTime.Now). |
DateUpdated |
DateTime |
<DateUpdated> |
When this record was last updated (set by LoadInstance() and LoadCapabilities()). |
IsEnabled |
bool |
<IsEnabled> |
Whether this device is enabled for mapping (default: true). |
IsHidden |
bool |
<IsHidden> |
Whether this device is hidden from the UI. Device remains in SettingsManager but is filtered from the device list. |
DisplayName |
string |
<DisplayName> |
User-assigned display name. Overrides InstanceName in the UI when set. |
HidHideEnabled |
bool |
<HidHideEnabled> |
Hide this device from games via HidHide driver (default: false). |
ConsumeInputEnabled |
bool |
<ConsumeInputEnabled> |
Suppress mapped inputs via low-level hooks (default: false). Only meaningful for keyboards and mice. |
| XML Element | Type | Default | Description |
|---|---|---|---|
HidHideEnabled |
bool |
false |
Hide device from games via HidHide |
ConsumeInputEnabled |
bool |
false |
Suppress mapped keyboard/mouse inputs via hooks |
ForceRawJoystickMode |
bool |
false |
Bypass SDL3 gamepad remapping |
HidHideInstanceIds |
string[] |
empty | Cached HID instance IDs for offline blacklisting |
| Property | Type | Description |
|---|---|---|
Device |
ISdlInputDevice |
The opened SDL device wrapper. Live handle for state reading and rumble. Set during Step 1, cleared on disconnect. |
IsOnline |
bool |
Whether the device is physically connected and opened. |
InputState |
CustomInputState |
Current input state snapshot. Written by background thread (Step 2), read by UI thread. Reference assignment is atomic. |
InputUpdates |
CustomInputUpdate[] |
Buffered input updates since last poll cycle. |
OldInputState |
CustomInputState |
Previous state for change detection. |
OrgInputState |
CustomInputState |
Original state captured at recording start (for recorder delta detection). |
AxeMask |
int |
Bitmask of present axes (bit N set = axis N exists). |
ActuatorMask |
int |
Bitmask of force-feedback actuator axes. |
ActuatorCount |
int |
Total number of force-feedback actuator axes. |
SliderMask |
int |
Bitmask of present sliders. |
DeviceObjects |
DeviceObjectItem[] |
Axis/button/hat metadata (names, types). Populated during Step 1. |
DeviceEffects |
DeviceEffectItem[] |
Rumble capability metadata. |
ForceFeedbackState |
ForceFeedbackState |
Force feedback / haptic state tracker. Created for devices with rumble or haptic support. |
| Property | Type | Description |
|---|---|---|
IsMouse |
bool |
CapType == InputDeviceType.Mouse |
IsKeyboard |
bool |
CapType == InputDeviceType.Keyboard |
HasForceFeedback |
bool |
ActuatorCount > 0 || Device.HasRumble || Device.HasHaptic |
ResolvedName |
string |
Display name resolution: DisplayName > InstanceName > ProductName > "(Unknown Device)" |
StatusText |
string |
Status string: "Disabled", "Online", or "Offline" |
public void LoadFromSdlDevice(SdlDeviceWrapper wrapper)Main entry point for joystick/gamepad devices. Calls LoadFromDevice() (shared logic) and additionally sets DevRevision.
public void LoadFromKeyboardDevice(SdlKeyboardWrapper wrapper)
public void LoadFromMouseDevice(SdlMouseWrapper wrapper)Entry points for keyboard and mouse. Call LoadFromDevice() directly.
private void LoadFromDevice(ISdlInputDevice wrapper)Shared logic for all device types:
- Calls
LoadInstance()with identity values (InstanceGuid, Name, ProductGuid) - Calls
LoadCapabilities()with capability values (axes, buttons, hats, type) - Sets
RawButtonCount = Math.Max(wrapper.RawButtonCount, wrapper.NumButtons) - Sets sensor flags (
HasGyro,HasAccel) - Populates
VendorId,ProdId,DevicePath,SerialNumber - Builds
DeviceObjectsandDeviceEffects - Computes
AxeMask,ActuatorMask,SliderMaskfrom device objects - Creates
ForceFeedbackStateif device has rumble or haptic support - Stores the wrapper as
Device
public void ClearRuntimeState()Called on disconnect. Nulls all runtime fields (Device, InputState, DeviceObjects, etc.), sets IsOnline = false, raises NotifyStateChanged().
File: PadForge.Engine/Data/UserSetting.cs
Namespace: PadForge.Engine.Data
Implements: INotifyPropertyChanged
Links a physical input device (identified by InstanceGuid) to a virtual controller slot (identified by MapTo) and a mapping configuration (identified by PadSettingChecksum). One UserSetting per device-to-slot assignment.
| Property | Type | XML Element | Default | Description |
|---|---|---|---|---|
InstanceGuid |
Guid |
<InstanceGuid> |
Guid.Empty |
Device instance GUID. Must match UserDevice.InstanceGuid. |
InstanceName |
string |
<InstanceName> |
"" |
Human-readable name at creation time. Used for display when the device is offline. |
ProductGuid |
Guid |
<ProductGuid> |
Guid.Empty |
Product GUID for fallback matching when instance GUIDs change (e.g., different USB port, BT reconnect). |
ProductName |
string |
<ProductName> |
"" |
Human-readable product name. |
MapTo |
int |
<MapTo> |
-1 |
Virtual controller slot index (0-15). -1 = unmapped. Fires PropertyChanged. |
PadSettingChecksum |
string |
<PadSettingChecksum> |
"" |
Checksum that links to a PadSetting record. Multiple UserSettings can share the same checksum. |
IsEnabled |
bool |
<IsEnabled> |
true |
Whether this device-to-slot mapping is active. Disabled mappings are skipped in the pipeline. |
DateCreated |
DateTime |
<DateCreated> |
DateTime.Now |
When this setting was created. |
DateUpdated |
DateTime |
<DateUpdated> |
DateTime.Now |
When this setting was last modified. |
| Property | Type | Description |
|---|---|---|
OutputState |
Gamepad |
Per-device mapped output state computed in Step 3. Written by background thread, read by Step 4 and UI. |
RawMappedState |
Gamepad |
Raw mapped state: axis-selected and Y-negated but BEFORE center offset, dead zone, anti-dead zone, linear, and max range processing. Used by the UI preview to apply its own pipeline without double-processing. |
VJoyRawOutputState |
VJoyRawState |
Per-device raw vJoy output state for custom vJoy configurations. Only populated when the slot uses vJoy with Custom preset. |
MidiRawOutputState |
MidiRawState |
Per-device MIDI raw output state. Only populated when the slot uses MIDI output type. |
KbmRawOutputState |
KbmRawState |
Per-device Keyboard+Mouse raw output state. Only populated when the slot uses KeyboardMouse output type. |
_cachedPadSetting |
PadSetting (internal) |
Cached PadSetting reference. Set by SettingsManager/SettingsService during load. Accessed via GetPadSetting() and SetPadSetting(). |
Multiple UserSettings can share the same InstanceGuid with different MapTo values. This enables one physical device to feed multiple virtual controllers simultaneously. Each assignment has its own independent PadSetting (cloned during load to prevent shared-mutation bugs).
Example: A DualSense controller assigned to both Player 1 (slot 0) and Player 3 (slot 2) produces two UserSetting entries:
{ InstanceGuid = "abc...", MapTo = 0, PadSettingChecksum = "X1Y2Z3A4" }{ InstanceGuid = "abc...", MapTo = 2, PadSettingChecksum = "B5C6D7E8" }
File: PadForge.Engine/Data/PadSetting.cs
Namespace: PadForge.Engine.Data
Partial class
Contains the complete mapping configuration for a device-to-slot assignment. All mapping properties are string-typed descriptors in the format consumed by InputManager Step 3.
[XmlElement]
public string PadSettingChecksum { get; set; } = "";The checksum is an 8-character uppercase hex string derived from an MD5 hash of all mapping and setting properties. It serves three purposes:
- Linking: UserSettings reference PadSettings by checksum (not by array index or GUID)
- Deduplication: During save, PadSettings with the same checksum are serialized only once
- Change detection: Any change to any property produces a different checksum
ComputeChecksum() builds a pipe-delimited string from all properties that affect the mapping behavior, in a fixed order:
- Button mappings (11): ButtonA, ButtonB, ButtonX, ButtonY, LeftShoulder, RightShoulder, ButtonBack, ButtonStart, ButtonGuide, LeftThumbButton, RightThumbButton
- D-Pad (5): DPad, DPadUp, DPadDown, DPadLeft, DPadRight
- Triggers (8): LeftTrigger, RightTrigger, LeftTriggerDeadZone, RightTriggerDeadZone, LeftTriggerAntiDeadZone, RightTriggerAntiDeadZone, LeftTriggerMaxRange, RightTriggerMaxRange
- Thumbstick axes (8): LeftThumbAxisX, LeftThumbAxisY, RightThumbAxisX, RightThumbAxisY, LeftThumbAxisXNeg, LeftThumbAxisYNeg, RightThumbAxisXNeg, RightThumbAxisYNeg
- Dead zones (22): LeftThumbDeadZoneX/Y, RightThumbDeadZoneX/Y, LeftThumbDeadZoneShape, RightThumbDeadZoneShape, LeftThumbAntiDeadZone, RightThumbAntiDeadZone, LeftThumbAntiDeadZoneX/Y, RightThumbAntiDeadZoneX/Y, LeftThumbLinear, RightThumbLinear, all 6 sensitivity curves, all 8 max range properties, all 4 center offset properties
- Force feedback (5): ForceType, ForceOverall, ForceSwapMotor, LeftMotorStrength, RightMotorStrength
- Audio bass rumble (5): AudioRumbleEnabled, AudioRumbleSensitivity, AudioRumbleCutoffHz, AudioRumbleLeftMotor, AudioRumbleRightMotor
- Axis inversion (4): LeftThumbAxisXInvert, LeftThumbAxisYInvert, RightThumbAxisXInvert, RightThumbAxisYInvert
- Threshold (1): AxisToButtonThreshold
-
vJoy custom mappings -- Dictionary entries sorted by key (
StringComparer.Ordinal), formatted askey=value| - MIDI custom mappings -- Same sorted key=value format
- KBM custom mappings -- Same sorted key=value format
The pipe-delimited string is UTF-8 encoded, hashed with MD5.HashData(), and the first 4 bytes are returned as an 8-character uppercase hex string:
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
return BitConverter.ToString(hash, 0, 4).Replace("-", "").ToUpperInvariant();Not included in the checksum: PadSettingChecksum itself, GameFileName.
During SaveToFile(), PadSettings are deduplicated by their checksum before serialization. A HashSet<string> tracks seen checksums:
var seen = new HashSet<string>();
var uniquePadSettings = new List<PadSetting>();
foreach (var us in UserSettings.Items)
{
var ps = us.GetPadSetting();
if (ps != null && seen.Add(ps.PadSettingChecksum))
uniquePadSettings.Add(ps);
}
data.PadSettings = uniquePadSettings.ToArray();This means the XML file stores N unique PadSetting elements where N <= number of UserSettings. Multiple <Setting> elements reference the same <PadSettingChecksum> value.
All mapping properties use string descriptors understood by the InputManager Step 3 mapping engine:
| Format | Example | Description |
|---|---|---|
"Button N" |
"Button 0" |
Button at index N |
"Axis N" |
"Axis 1" |
Full-range axis at index N |
"IAxis N" |
"IAxis 1" |
Inverted axis (I prefix) |
"HAxis N" |
"HAxis 2" |
Half-axis, 0-100% range (H prefix) |
"IHAxis N" |
"IHAxis 2" |
Inverted half-axis (IH prefix) |
"Slider N" |
"Slider 0" |
Slider at index N |
"POV N Dir" |
"POV 0 Up" |
POV hat at index N, direction: Up, Down, Left, Right, UpRight, UpLeft, DownRight, DownLeft |
"" |
"" |
Unmapped (empty string) |
Prefix meanings:
- I (Invert): Flips the axis direction. Applied by the recorder's auto-inversion logic.
- H (Half): Maps a full-range axis to a 0-100% range (used for triggers and unidirectional mappings).
All [XmlElement], all string, all default to "":
| Property | Description |
|---|---|
ButtonA |
A / Cross button |
ButtonB |
B / Circle button |
ButtonX |
X / Square button |
ButtonY |
Y / Triangle button |
LeftShoulder |
LB / L1 |
RightShoulder |
RB / R1 |
ButtonBack |
Back / Share / Select |
ButtonStart |
Start / Options |
ButtonGuide |
Guide / PS button |
LeftThumbButton |
LS / L3 (left stick press) |
RightThumbButton |
RS / R3 (right stick press) |
| Property | Default | Description |
|---|---|---|
DPad |
"" |
Combined D-Pad mapping. If set to "POV 0", all four directions are auto-extracted by Step 3. |
DPadUp |
"" |
Override: D-Pad up direction |
DPadDown |
"" |
Override: D-Pad down direction |
DPadLeft |
"" |
Override: D-Pad left direction |
DPadRight |
"" |
Override: D-Pad right direction |
When individual DPadUp/Down/Left/Right properties are set, they override the combined DPad property.
| Property | Default | Description |
|---|---|---|
LeftTrigger |
"" |
Left trigger source mapping |
RightTrigger |
"" |
Right trigger source mapping |
LeftTriggerDeadZone |
"0" |
Dead zone 0-100. Values below this percentage are treated as zero. |
RightTriggerDeadZone |
"0" |
Dead zone 0-100. |
LeftTriggerAntiDeadZone |
"0" |
Anti-dead zone 0-100%. Offsets the output range minimum so small physical presses register past the game's built-in dead zone. |
RightTriggerAntiDeadZone |
"0" |
Anti-dead zone 0-100%. |
LeftTriggerMaxRange |
"100" |
Max range 1-100%. Caps the output ceiling so full physical press maps to this percentage. |
RightTriggerMaxRange |
"100" |
Max range 1-100%. |
| Property | Default | Description |
|---|---|---|
LeftThumbAxisX |
"" |
Left stick X positive direction |
LeftThumbAxisY |
"" |
Left stick Y positive direction |
RightThumbAxisX |
"" |
Right stick X positive direction |
RightThumbAxisY |
"" |
Right stick Y positive direction |
LeftThumbAxisXNeg |
"" |
Left stick X negative direction (for button-to-axis mappings) |
LeftThumbAxisYNeg |
"" |
Left stick Y negative direction |
RightThumbAxisXNeg |
"" |
Right stick X negative direction |
RightThumbAxisYNeg |
"" |
Right stick Y negative direction |
The "Neg" variants enable mapping separate physical inputs to opposite directions of a single virtual axis (e.g., mapping two buttons to left/right on a stick).
| Property | Default | Description |
|---|---|---|
LeftThumbDeadZoneX |
"0" |
Left stick dead zone X axis (0-100%) |
LeftThumbDeadZoneY |
"0" |
Left stick dead zone Y axis (0-100%) |
RightThumbDeadZoneX |
"0" |
Right stick dead zone X axis (0-100%) |
RightThumbDeadZoneY |
"0" |
Right stick dead zone Y axis (0-100%) |
LeftThumbAntiDeadZone |
"0" |
Legacy unified anti-dead zone (migrated to per-axis on load) |
RightThumbAntiDeadZone |
"0" |
Legacy unified anti-dead zone |
LeftThumbAntiDeadZoneX |
"0" |
Left stick anti-dead zone X (0-100%) |
LeftThumbAntiDeadZoneY |
"0" |
Left stick anti-dead zone Y (0-100%) |
RightThumbAntiDeadZoneX |
"0" |
Right stick anti-dead zone X (0-100%) |
RightThumbAntiDeadZoneY |
"0" |
Right stick anti-dead zone Y (0-100%) |
LeftThumbLinear |
"0" |
Left stick linear response curve (0-100). 0 = default curve, 100 = fully linear. |
RightThumbLinear |
"0" |
Right stick linear response curve (0-100). |
| Property | Default | Description |
|---|---|---|
LeftThumbDeadZoneShape |
"2" |
Left stick dead zone shape (DeadZoneShape enum, see below). Default 2 = ScaledRadial. |
RightThumbDeadZoneShape |
"2" |
Right stick dead zone shape. |
DeadZoneShape enum (PadForge.Engine/Data/DeadZoneShape.cs):
| Value | Name | Description |
|---|---|---|
| 0 | Axial |
Independent per-axis deadzone (square/cross shape). Legacy behavior. |
| 1 | Radial |
Circular/elliptical magnitude check, no output rescaling. |
| 2 | ScaledRadial |
Circular/elliptical magnitude check with output rescaling (industry standard). Default. |
| 3 | SlopedAxial |
Axis-dependent thresholds: DZ grows on one axis as the other increases. |
| 4 | SlopedScaledAxial |
Sloped axis-dependent thresholds with output rescaling. |
| 5 | Hybrid |
Scaled Radial followed by Sloped Scaled Axial (best hybrid). |
| Property | Default | Description |
|---|---|---|
LeftThumbSensitivityCurveX |
"0" |
Left stick X-axis sensitivity curve. Format: "0,0;0.5,0.2;1,1" (semicolon-separated input,output control point pairs). "0" or "0,0;1,1" = linear. |
LeftThumbSensitivityCurveY |
"0" |
Left stick Y-axis sensitivity curve. |
RightThumbSensitivityCurveX |
"0" |
Right stick X-axis sensitivity curve. |
RightThumbSensitivityCurveY |
"0" |
Right stick Y-axis sensitivity curve. |
LeftTriggerSensitivityCurve |
"0" |
Left trigger sensitivity curve (same format). |
RightTriggerSensitivityCurve |
"0" |
Right trigger sensitivity curve. |
| Property | Default | Description |
|---|---|---|
LeftThumbCenterOffsetX |
"0" |
Left stick X center offset (-100 to 100%). Raw offset value subtracted before dead zone processing. |
LeftThumbCenterOffsetY |
"0" |
Left stick Y center offset |
RightThumbCenterOffsetX |
"0" |
Right stick X center offset |
RightThumbCenterOffsetY |
"0" |
Right stick Y center offset |
LeftThumbMaxRangeX |
"100" |
Left stick X max range (1-100%). Full physical deflection maps to this output ceiling. |
LeftThumbMaxRangeY |
"100" |
Left stick Y max range |
RightThumbMaxRangeX |
"100" |
Right stick X max range |
RightThumbMaxRangeY |
"100" |
Right stick Y max range |
LeftThumbMaxRangeXNeg |
(null) | Left stick X negative direction (left) max range (1-100%). Null = symmetric with positive direction. |
LeftThumbMaxRangeYNeg |
(null) | Left stick Y negative direction (down) max range. Null = symmetric. |
RightThumbMaxRangeXNeg |
(null) | Right stick X negative direction max range. Null = symmetric. |
RightThumbMaxRangeYNeg |
(null) | Right stick Y negative direction max range. Null = symmetric. |
| Property | Default | Description |
|---|---|---|
ForceType |
"1" |
Force feedback type. 0 = Off, 1 = SDL Rumble. |
ForceOverall |
"100" |
Overall gain (0-100%). Multiplier applied to both motors. |
ForceSwapMotor |
"0" |
Swap left/right motors. "0" = no swap, "1" = swap. |
LeftMotorStrength |
"100" |
Left (low-frequency) motor strength (0-100%). |
RightMotorStrength |
"100" |
Right (high-frequency) motor strength (0-100%). |
| Property | Default | Description |
|---|---|---|
AudioRumbleEnabled |
"0" |
Enable audio bass rumble. "0" = off, "1" = on. |
AudioRumbleSensitivity |
"4" |
Bass detection sensitivity (higher = more reactive). |
AudioRumbleCutoffHz |
"80" |
Low-pass cutoff frequency in Hz for bass extraction. |
AudioRumbleLeftMotor |
"100" |
Left motor strength percentage for audio rumble (0-100). |
AudioRumbleRightMotor |
"100" |
Right motor strength percentage for audio rumble (0-100). |
| Property | Default | Description |
|---|---|---|
LeftThumbAxisXInvert |
"0" |
Invert left stick X axis. "0" or "1". |
LeftThumbAxisYInvert |
"0" |
Invert left stick Y axis. |
RightThumbAxisXInvert |
"0" |
Invert right stick X axis. |
RightThumbAxisYInvert |
"0" |
Invert right stick Y axis. |
AxisToButtonThreshold |
"50" |
Threshold (0-100%) for treating an axis as a button press. Axis must exceed this percentage to register as pressed. |
GameFileName |
"" |
Optional game executable name for game-specific settings. Empty = global. |
Three mapping types use dictionary-based storage for arbitrary key counts: vJoy, MIDI, and Keyboard+Mouse. All three share the same serialization entry type (VJoyMappingEntry) and follow the same pattern: an in-memory Dictionary<string, string> backed by a serializable VJoyMappingEntry[] array. The dictionary is populated lazily on first access and flushed to the array before serialization.
Custom vJoy configurations support arbitrary axis/button/POV counts. Keys:
-
VJoyAxis0,VJoyAxis0Neg-- axis mappings (positive and negative directions) -
VJoyBtn0,VJoyBtn5-- button mappings -
VJoyPov0Up,VJoyPov0Down,VJoyPov0Left,VJoyPov0Right-- POV directions -
VJoyStick{N}DzX,VJoyStick{N}DzY,VJoyStick{N}AdzX, etc. -- per-stick dead zone/calibration settings
[XmlArray("VJoyMappings")]
[XmlArrayItem("Map")]
public VJoyMappingEntry[] VJoyMappingEntries { get; set; }MIDI output with configurable CC/note ranges. Keys:
-
MidiCC0,MidiCC0Neg-- CC (Control Change) mappings for axes -
MidiNote0,MidiNote5-- Note mappings for buttons
[XmlArray("MidiMappings")]
[XmlArrayItem("Map")]
public VJoyMappingEntry[] MidiMappingEntries { get; set; }Keyboard+Mouse output. Keys:
-
KbmKey{VK:X2}-- keyboard key by virtual key code hex (e.g.,KbmKey41= VK_A) -
KbmMouseX,KbmMouseXNeg,KbmMouseY,KbmMouseYNeg-- mouse movement axes -
KbmMBtn0..KbmMBtn4-- mouse buttons (LMB, RMB, MMB, X1, X2) -
KbmScroll,KbmScrollNeg-- mouse scroll wheel
[XmlArray("KbmMappings")]
[XmlArrayItem("Map")]
public VJoyMappingEntry[] KbmMappingEntries { get; set; }public class VJoyMappingEntry
{
[XmlAttribute] public string Key { get; set; } = "";
[XmlAttribute] public string Value { get; set; } = "";
}| Method | Description |
|---|---|
string Get{Type}Mapping(string key) |
Get descriptor by key. Returns empty string if not found. |
void Set{Type}Mapping(string key, string value) |
Set descriptor by key. Empty/null values remove the entry. |
void Flush{Type}Mappings() |
Flush dictionary to the serializable array. Must be called before serialization. |
Where {Type} is VJoy, Midi, or Kbm. The dictionary is initialized lazily from the array on first access via Ensure{Type}Dict(), which is thread-safe (uses double-checked locking).
The CopyablePropertyNames static array defines which properties participate in CopyFrom(), ToJson(), and FromJson(). It includes all user-facing configuration properties but excludes identity and metadata fields.
Complete list (73 properties):
| Category | Properties |
|---|---|
| Buttons (11) |
ButtonA, ButtonB, ButtonX, ButtonY, LeftShoulder, RightShoulder, ButtonBack, ButtonStart, ButtonGuide, LeftThumbButton, RightThumbButton
|
| D-Pad (5) |
DPad, DPadUp, DPadDown, DPadLeft, DPadRight
|
| Triggers (8) |
LeftTrigger, RightTrigger, LeftTriggerDeadZone, RightTriggerDeadZone, LeftTriggerAntiDeadZone, RightTriggerAntiDeadZone, LeftTriggerMaxRange, RightTriggerMaxRange
|
| Stick axes (8) |
LeftThumbAxisX, LeftThumbAxisY, RightThumbAxisX, RightThumbAxisY, LeftThumbAxisXNeg, LeftThumbAxisYNeg, RightThumbAxisXNeg, RightThumbAxisYNeg
|
| Dead zones (14) |
LeftThumbDeadZoneX, LeftThumbDeadZoneY, RightThumbDeadZoneX, RightThumbDeadZoneY, LeftThumbDeadZoneShape, RightThumbDeadZoneShape, LeftThumbAntiDeadZone, RightThumbAntiDeadZone, LeftThumbAntiDeadZoneX, LeftThumbAntiDeadZoneY, RightThumbAntiDeadZoneX, RightThumbAntiDeadZoneY, LeftThumbLinear, RightThumbLinear
|
| Sensitivity curves (6) |
LeftThumbSensitivityCurveX, LeftThumbSensitivityCurveY, RightThumbSensitivityCurveX, RightThumbSensitivityCurveY, LeftTriggerSensitivityCurve, RightTriggerSensitivityCurve
|
| Max range (8) |
LeftThumbMaxRangeX, LeftThumbMaxRangeY, RightThumbMaxRangeX, RightThumbMaxRangeY, LeftThumbMaxRangeXNeg, LeftThumbMaxRangeYNeg, RightThumbMaxRangeXNeg, RightThumbMaxRangeYNeg
|
| Center offset (4) |
LeftThumbCenterOffsetX, LeftThumbCenterOffsetY, RightThumbCenterOffsetX, RightThumbCenterOffsetY
|
| Force feedback (5) |
ForceType, ForceOverall, ForceSwapMotor, LeftMotorStrength, RightMotorStrength
|
| Audio bass rumble (5) |
AudioRumbleEnabled, AudioRumbleSensitivity, AudioRumbleCutoffHz, AudioRumbleLeftMotor, AudioRumbleRightMotor
|
| Axis inversion (4) |
LeftThumbAxisXInvert, LeftThumbAxisYInvert, RightThumbAxisXInvert, RightThumbAxisYInvert
|
| Threshold (1) | AxisToButtonThreshold |
Excluded from CopyablePropertyNames:
-
PadSettingChecksum-- identity, recomputed after copy -
GameFileName-- per-game metadata, not part of mapping configuration -
VJoyMappingEntries,MidiMappingEntries,KbmMappingEntries-- handled separately via dictionary deep-copy inCopyFrom()and JSON serialization inToJson()/FromJson()
Usage:
-
CopyFrom(PadSetting source)-- IteratesCopyablePropertyNamesvia reflection, copies each string property value, then deep-copies the three mapping arrays -
ToJson()-- Serializes allCopyablePropertyNamesplus__VJoyMappings/__MidiMappings/__KbmMappingsarrays and layout metadata (__OutputType,__IsCustomVJoy) to a JSON dictionary for clipboard copy -
FromJson(string json)-- Deserializes JSON back into a PadSetting, parsing layout metadata for cross-layout paste support
| Method | Signature | Description |
|---|---|---|
CloneDeep |
PadSetting CloneDeep() |
Deep clone. Calls CopyFrom(this) on a new instance, then copies PadSettingChecksum and GameFileName. Dictionary arrays are deep-copied. |
CopyFrom |
void CopyFrom(PadSetting source) |
Copies all CopyablePropertyNames from source using reflection. Flushes source dictionaries, then deep-copies VJoyMappingEntries, MidiMappingEntries, KbmMappingEntries arrays and invalidates cached dictionaries. |
CopyFromTranslated |
void CopyFromTranslated(PadSetting source, ...) |
Cross-layout copy with positional translation (see Cross-Layout Mapping Translation). |
ToJson |
string ToJson(VirtualControllerType, bool) |
Serializes to JSON dictionary for clipboard. Includes layout metadata. |
FromJson |
static PadSetting FromJson(string json, out ...) |
Deserializes JSON to new PadSetting. Returns null on invalid input. Extracts layout metadata. |
ClearMappingDescriptors |
void ClearMappingDescriptors() |
Clears all mapping descriptors (standard + vJoy + MIDI + KBM dictionaries) while preserving dead zone, force feedback, and other non-mapping configuration. |
GetAllMappingDescriptors |
List<string> GetAllMappingDescriptors() |
Returns all non-empty mapping descriptor strings from standard properties, vJoy, and MIDI entries. |
HasAnyMapping |
bool (property) |
Returns true if at least one mapping property has a non-empty descriptor (checks standard, vJoy, and MIDI). |
| Method | Description |
|---|---|
MigrateAntiDeadZones() |
Migrates legacy unified LeftThumbAntiDeadZone/RightThumbAntiDeadZone to per-axis X/Y properties. Only migrates when per-axis values are empty/zero and unified value is non-zero. Idempotent. |
MigrateMaxRangeDirections() |
Migrates symmetric max range values to per-direction properties. If LeftThumbMaxRangeXNeg is null/empty, copies the value from LeftThumbMaxRangeX. Same for all four negative-direction properties. |
File: PadForge.App/Services/SettingsService.cs (inner class)
Application-level settings stored as a single <AppSettings> element inside the root.
| Property | Type | XML Serialization | Default | Description |
|---|---|---|---|---|
AutoStartEngine |
bool |
[XmlElement] |
true |
Auto-start the InputManager engine on application launch |
MinimizeToTray |
bool |
[XmlElement] |
false |
Minimize to system tray instead of taskbar |
StartMinimized |
bool |
[XmlElement] |
false |
Start the application minimized |
StartAtLogin |
bool |
[XmlElement] |
false |
Register as a Windows startup application |
EnablePollingOnFocusLoss |
bool |
[XmlElement] |
true |
Continue polling when the application loses focus |
PollingRateMs |
int |
[XmlElement] |
1 |
Polling interval in milliseconds (~1000Hz at 1ms) |
ThemeIndex |
int |
[XmlElement] |
0 |
UI theme selection index |
Language |
string |
[XmlElement] |
"" |
UI language code (e.g., "en", "fr", "ja"). Empty = system default. |
EnableAutoProfileSwitching |
bool |
[XmlElement] |
false |
Enable foreground-based automatic profile switching |
ActiveProfileId |
string |
[XmlElement] |
null |
ID of the currently active named profile (null = default) |
SlotControllerTypes |
int[] |
[XmlArray("SlotControllerTypes")][XmlArrayItem("Type")] |
null |
Per-slot VirtualControllerType enum values (0=Xbox360, 1=DualShock4, 2=VJoy, 3=Midi, 4=KeyboardMouse) |
SlotCreated |
bool[] |
[XmlArray("SlotCreated")][XmlArrayItem("Created")] |
null |
Which virtual controller slots are explicitly created |
SlotEnabled |
bool[] |
[XmlArray("SlotEnabled")][XmlArrayItem("Enabled")] |
null |
Which slots are enabled for output |
EnableDsuMotionServer |
bool |
[XmlElement] |
false |
Enable the DSU/Cemuhook motion server |
DsuMotionServerPort |
int |
[XmlElement] |
26760 |
DSU server listening port |
EnableWebController |
bool |
[XmlElement] |
false |
Enable the embedded web controller server |
WebControllerPort |
int |
[XmlElement] |
8080 |
Web controller HTTP/WebSocket listening port |
Use2DControllerView |
bool |
[XmlElement] |
false |
Use 2D controller visualization instead of 3D |
EnableInputHiding |
bool |
[XmlElement] |
true |
Master switch for all input hiding (HidHide + hooks). When false, no hiding occurs regardless of per-device toggles. |
HidHideWhitelistPaths |
string[] |
[XmlArray("HidHideWhitelistPaths")][XmlArrayItem("Path")] |
null |
Application paths whitelisted in HidHide. Null when empty (no element written to XML). |
VJoyConfigs |
VJoySlotConfigData[] |
[XmlArray("VJoyConfigs")][XmlArrayItem("Config")] |
null |
Per-slot vJoy configuration (preset, axis/button/POV counts) |
MidiConfigs |
MidiSlotConfigData[] |
[XmlArray("MidiConfigs")][XmlArrayItem("Config")] |
null |
Per-slot MIDI configuration (channel, CC/note ranges, velocity) |
DefaultProfileSnapshot |
ProfileData |
[XmlElement("DefaultProfileSnapshot")] |
null |
Full snapshot of the default profile's state. Only populated when a named profile is active; null when the default profile is active (its state is in the global fields). See Default Profile Snapshot. |
When a named profile is active at save time, the root-level SlotCreated, SlotEnabled, SlotControllerTypes, VJoyConfigs, and MidiConfigs fields must still represent the default profile's state (not the active named profile's). Without this, switching back to the default profile would lose its original topology.
The mechanism works as follows:
During Save (BuildAppSettings()):
- If the default profile is active (
ActiveProfileIdis null): slot arrays come from the live runtime state,DefaultProfileSnapshotis null. - If a named profile is active: slot arrays come from
SettingsManager.PendingDefaultSnapshot(the saved default state), andDefaultProfileSnapshotis set to that snapshot so it survives restart.
During Load (LoadProfiles()):
- If a named profile was active at shutdown:
SettingsManager.PendingDefaultSnapshotis restored fromappSettings.DefaultProfileSnapshot. The named profile's topology (SlotCreated,SlotEnabled,SlotControllerTypes, vJoy/MIDI configs) is then applied over the default's loaded values. -
InputService.Start()usesPendingDefaultSnapshotto initialize_defaultProfileSnapshotcorrectly.
public class VJoySlotConfigData
{
[XmlAttribute] public int SlotIndex { get; set; }
[XmlElement] public VJoyPreset Preset { get; set; }
[XmlElement] public int ThumbstickCount { get; set; }
[XmlElement] public int TriggerCount { get; set; }
[XmlElement] public int PovCount { get; set; }
[XmlElement] public int ButtonCount { get; set; }
}File: PadForge.App/ViewModels/MidiSlotConfig.cs
Serializable DTO for per-slot MIDI configuration. Stored in <MidiConfigs> array.
public class MidiSlotConfigData
{
[XmlAttribute] public int SlotIndex { get; set; }
[XmlAttribute] public int Channel { get; set; } = 1;
[XmlAttribute] public int CcCount { get; set; } = 6;
[XmlAttribute] public int StartCc { get; set; } = 1;
[XmlAttribute] public int NoteCount { get; set; } = 11;
[XmlAttribute] public int StartNote { get; set; } = 60;
[XmlAttribute] public int Velocity { get; set; } = 127;
}| Property | Default | Description |
|---|---|---|
SlotIndex |
-- | Zero-based pad slot index |
Channel |
1 |
MIDI channel (1-16, displayed as 1-based) |
CcCount |
6 |
Number of CC messages to send (maps to axes) |
StartCc |
1 |
First CC number in the sequential range |
NoteCount |
11 |
Number of notes to send (maps to buttons) |
StartNote |
60 |
First MIDI note number (Middle C) |
Velocity |
127 |
Note-on velocity (0-127) |
Serializable DTO for a macro configuration. Stored per pad slot via the PadIndex attribute.
public class MacroData
{
[XmlAttribute] public int PadIndex { get; set; }
[XmlElement] public string Name { get; set; } = "New Macro";
[XmlElement] public bool IsEnabled { get; set; } = true;
[XmlElement] public ushort TriggerButtons { get; set; }
[XmlElement] public string TriggerDeviceGuid { get; set; }
[XmlElement] public string TriggerRawButtons { get; set; }
[XmlElement] public MacroTriggerSource TriggerSource { get; set; }
[XmlElement] public MacroTriggerMode TriggerMode { get; set; }
[XmlElement] public bool ConsumeTriggerButtons { get; set; } = true;
[XmlElement] public MacroRepeatMode RepeatMode { get; set; }
[XmlElement] public int RepeatCount { get; set; } = 1;
[XmlElement] public int RepeatDelayMs { get; set; } = 100;
[XmlElement] public string TriggerCustomButtons { get; set; }
[XmlElement] public string TriggerAxisTargets { get; set; }
[XmlElement] public int TriggerAxisThreshold { get; set; } = 50;
[XmlArray("TriggerPovs")]
[XmlArrayItem("Pov")]
public string[] TriggerPovs { get; set; }
[XmlArray("Actions")]
[XmlArrayItem("Action")]
public ActionData[] Actions { get; set; }
}| Property | Type | Description |
|---|---|---|
PadIndex |
int (attribute) |
Which pad slot this macro belongs to |
TriggerButtons |
ushort |
Xbox button bitmask for the trigger combo |
TriggerDeviceGuid |
string |
Device GUID for raw button trigger (N format, no hyphens) |
TriggerRawButtons |
string |
Comma-separated raw button indices (e.g., "13,14") |
TriggerSource |
MacroTriggerSource |
OutputController or InputDevice
|
TriggerMode |
MacroTriggerMode |
Press, Hold, or Toggle
|
ConsumeTriggerButtons |
bool |
Whether trigger buttons are consumed (removed from output) |
RepeatMode |
MacroRepeatMode |
Once, Count, or WhileHeld
|
TriggerCustomButtons |
string |
Hex-encoded vJoy button words (e.g., "00000003,...") |
TriggerAxisTargets |
string |
Comma-separated axis names for combo trigger (e.g., "LeftStickX,LeftTrigger") |
TriggerAxisThreshold |
int |
Axis threshold percentage (1-100, default 50). Normalized axis value must exceed this to match. |
TriggerPovs |
string[] |
POV trigger directions as "povIndex:centidegrees" strings (e.g., "0:0" for POV 0 Up). Serialized via XmlArray. |
public class ActionData
{
[XmlElement] public MacroActionType Type { get; set; }
[XmlElement] public ushort ButtonFlags { get; set; }
[XmlElement] public string CustomButtons { get; set; }
[XmlElement] public int KeyCode { get; set; }
[XmlElement] public string KeyString { get; set; }
[XmlElement] public int DurationMs { get; set; } = 50;
[XmlElement] public short AxisValue { get; set; }
[XmlElement] public MacroAxisTarget AxisTarget { get; set; }
[XmlElement] public MacroAxisSource AxisSource { get; set; }
[XmlElement] public string SourceDeviceGuid { get; set; }
[XmlElement] public int SourceDeviceAxisIndex { get; set; }
[XmlElement] public string ProcessName { get; set; }
[XmlElement] public int VolumeLimit { get; set; } = 100;
[XmlElement] public float MouseSensitivity { get; set; } = 10f;
[XmlElement] public MacroMouseButton MouseButton { get; set; }
[XmlElement] public bool InvertAxis { get; set; }
[XmlElement] public bool ShowVolumeOsd { get; set; } = true;
}| Property | Type | Default | Description |
|---|---|---|---|
Type |
MacroActionType |
-- |
Button, Key, Delay, Axis, SystemVolume, AppVolume, MouseMove, MouseScroll, MouseButtonPress, MouseButtonRelease, LaunchApp
|
ButtonFlags |
ushort |
0 |
Xbox button flags to press/release |
CustomButtons |
string |
null |
Hex-encoded vJoy button words |
KeyCode |
int |
0 |
Virtual key code (single key) |
KeyString |
string |
null |
Multi-key combo in {Key1}{Key2}... format (e.g., {LShiftKey}{A}). Takes precedence over KeyCode. |
DurationMs |
int |
50 |
How long to hold this action in milliseconds |
AxisValue |
short |
0 |
Axis value for axis actions |
AxisTarget |
MacroAxisTarget |
-- | Which axis to target (e.g., LeftThumbX) |
AxisSource |
MacroAxisSource |
-- | Where to read axis value from: OutputController (combined output) or InputDevice (physical device) |
SourceDeviceGuid |
string |
null |
GUID of the physical device when AxisSource == InputDevice (N format, no hyphens) |
SourceDeviceAxisIndex |
int |
0 |
Axis index on the source device when AxisSource == InputDevice
|
ProcessName |
string |
null |
Process name for AppVolume action (e.g., "firefox", "spotify") |
VolumeLimit |
int |
100 |
Maximum volume percentage for SystemVolume/AppVolume actions (1-100) |
MouseSensitivity |
float |
10 |
Pixels/scroll units per frame at full deflection for MouseMove/MouseScroll |
MouseButton |
MacroMouseButton |
-- | Which mouse button for MouseButtonPress/Release (Left, Right, Middle, X1, X2) |
InvertAxis |
bool |
false |
When true, invert the axis value (0 to 1 becomes 1 to 0) |
ShowVolumeOsd |
bool |
true |
When true, show the Windows volume flyout OSD on volume changes |
File: PadForge.App/Services/SettingsService.cs (inner class)
Per-application profile. Stores a complete snapshot of device assignments, PadSettings, macros, slot topology, vJoy/MIDI configs, and server settings.
public class ProfileData
{
[XmlAttribute] public string Id { get; set; } = Guid.NewGuid().ToString("N");
[XmlElement] public string Name { get; set; } = "New Profile";
[XmlElement] public string ExecutableNames { get; set; } = string.Empty;
[XmlArray("Entries")][XmlArrayItem("Entry")]
public ProfileEntry[] Entries { get; set; }
[XmlArray("ProfilePadSettings")][XmlArrayItem("PadSetting")]
public PadSetting[] PadSettings { get; set; }
[XmlArray("ProfileMacros")][XmlArrayItem("Macro")]
public MacroData[] Macros { get; set; }
[XmlArray("ProfileSlotCreated")][XmlArrayItem("Created")]
public bool[] SlotCreated { get; set; }
[XmlArray("ProfileSlotEnabled")][XmlArrayItem("Enabled")]
public bool[] SlotEnabled { get; set; }
[XmlArray("ProfileSlotControllerTypes")][XmlArrayItem("Type")]
public int[] SlotControllerTypes { get; set; }
[XmlArray("ProfileVJoyConfigs")][XmlArrayItem("VJoyConfig")]
public VJoySlotConfigData[] VJoyConfigs { get; set; }
[XmlArray("ProfileMidiConfigs")][XmlArrayItem("MidiConfig")]
public MidiSlotConfigData[] MidiConfigs { get; set; }
[XmlElement] public bool EnableDsuMotionServer { get; set; }
[XmlElement] public int DsuMotionServerPort { get; set; } = 26760;
[XmlElement] public bool EnableWebController { get; set; }
[XmlElement] public int WebControllerPort { get; set; } = 8080;
}| Property | Type | Description |
|---|---|---|
Id |
string (attribute) |
Unique profile identifier (GUID without hyphens) |
Name |
string |
Display name |
ExecutableNames |
string |
Pipe-separated full executable paths for auto-switching (e.g., C:\Games\game.exe|D:\Other\game2.exe) |
Entries |
ProfileEntry[] |
Device-to-slot assignments within this profile |
PadSettings |
PadSetting[] |
Deep-cloned PadSettings for this profile's devices (deduplicated by checksum) |
Macros |
MacroData[] |
Macros for this profile (per-slot via PadIndex) |
SlotCreated |
bool[] |
Slot topology snapshot. Null on old profiles -- topology skipped. |
SlotEnabled |
bool[] |
Slot enabled states. Null on old profiles. |
SlotControllerTypes |
int[] |
Per-slot controller types. Null on old profiles. |
VJoyConfigs |
VJoySlotConfigData[] |
Per-slot vJoy configurations saved with this profile |
MidiConfigs |
MidiSlotConfigData[] |
Per-slot MIDI configurations saved with this profile |
EnableDsuMotionServer |
bool |
DSU server state for this profile |
DsuMotionServerPort |
int |
DSU server port for this profile (default: 26760) |
EnableWebController |
bool |
Web controller server state for this profile |
WebControllerPort |
int |
Web controller port for this profile (default: 8080) |
Links a device to a slot within a profile snapshot:
public class ProfileEntry
{
[XmlElement] public Guid InstanceGuid { get; set; }
[XmlElement] public Guid ProductGuid { get; set; }
[XmlElement] public int MapTo { get; set; }
[XmlElement] public string PadSettingChecksum { get; set; }
}ProductGuid enables fallback matching when InstanceGuid changes (e.g., Bluetooth device reconnects to a different adapter, or device plugged into a different USB port).
Profiles are created and updated via the UpdateActiveProfileSnapshot() method, which captures a complete snapshot of the current runtime state:
-
Entries: Iterates all
UserSettings.Items, creating aProfileEntryfor each withInstanceGuid,ProductGuid,MapTo, andPadSettingChecksum. -
PadSettings: Collects deep clones (
CloneDeep()) of each device's PadSetting, deduplicated by checksum using aHashSet<string>. -
Slot topology: Clones
SlotCreatedandSlotEnabledarrays, collectsOutputTypefrom each PadViewModel. - Type-specific configs: Snapshots vJoy configs (only for created vJoy slots) and MIDI configs (only for created MIDI slots).
- Server settings: Captures DSU motion server and web controller enable/port states.
This snapshot is called during Save() after checksums have been recomputed, ensuring the profile always reflects the latest edits.
File: PadForge.App/Common/SettingsManager.cs
Namespace: PadForge.Common.Input
Static partial class (canonical partial in SettingsManager.cs; additional partial declarations in InputManager.Step1.UpdateDevices.cs for UserDevices/UserSettings property declarations and the DeviceCollection/SettingsCollection class definitions)
Central manager for device records and mapping settings. Shared between the background engine thread and the UI thread.
-
SettingsService.Initialize()creates the collections and loads from XML. -
InputManager.Step1adds/updates UserDevice records as devices connect/disconnect. -
InputService(UI thread) reads collections to sync ViewModels. -
SettingsService.Save()serializes collections to XML.
All access to UserDevices and UserSettings must be done inside a lock on the respective collection's SyncRoot:
lock (SettingsManager.UserDevices.SyncRoot)
{
// Safe to iterate/modify UserDevices.Items
}| Property | Type | Description |
|---|---|---|
UserDevices |
DeviceCollection |
All known physical devices. Declared in InputManager.Step1.UpdateDevices.cs. |
UserSettings |
SettingsCollection |
All device-to-slot assignments. Declared in InputManager.Step1.UpdateDevices.cs. |
Profiles |
List<ProfileData> |
All saved profiles. Empty list = no profiles configured. |
ActiveProfileId |
string |
ID of the currently active named profile, or null for the default profile. |
PendingDefaultSnapshot |
ProfileData |
Snapshot of the default profile's state captured during load, before a named profile's topology is applied. Used by InputService.Start to initialize _defaultProfileSnapshot on restart. |
EnableAutoProfileSwitching |
bool |
Whether auto-switching based on foreground application is enabled. |
SlotCreated |
bool[16] |
Which virtual controller slots have been explicitly created. Persisted to settings. |
SlotEnabled |
bool[16] |
Which slots are enabled for output. Default: all true. Persisted to settings. |
| Constant | Value | Description |
|---|---|---|
MaxXbox360Slots |
MaxPads (16) |
Maximum Xbox 360 virtual controllers |
MaxDS4Slots |
MaxPads (16) |
Maximum DualShock 4 virtual controllers |
MaxVJoySlots |
16 | Maximum vJoy virtual controllers |
MaxMidiSlots |
MaxPads (16) |
Maximum MIDI virtual controllers |
MaxKeyboardMouseSlots |
MaxPads (16) |
Maximum Keyboard+Mouse virtual controllers |
All five types share the same global limit of 16. The "Add Controller" button disappears when all 16 slots are in use.
Defined in InputManager.Step1.UpdateDevices.cs:
public class DeviceCollection
{
public List<UserDevice> Items { get; } = new();
public object SyncRoot { get; } = new();
}
public class SettingsCollection
{
public List<UserSetting> Items { get; } = new();
public object SyncRoot { get; } = new();
public UserSetting FindByInstanceGuid(Guid guid) { ... }
public List<UserSetting> FindByPadIndex(int padIndex) { ... }
}| Method | Description |
|---|---|
EnsureInitialized() |
Creates collections if null. Safe to call multiple times. |
FindDeviceByInstanceGuid(Guid) |
Thread-safe lookup. Returns null if not found. |
RemoveDevice(Guid) |
Removes device and all associated UserSettings. Thread-safe. Returns true if removed. |
| Method | Description |
|---|---|
FindSettingByInstanceGuid(Guid) |
Find first UserSetting for a device. Thread-safe. |
FindSettingByInstanceGuidAndSlot(Guid, int) |
Find UserSetting for a specific device+slot pair. Required for multi-slot devices. Thread-safe. |
AssignDeviceToSlot(Guid, int) |
Creates or returns existing UserSetting. Supports multi-slot: creates a new entry for each additional slot. Does NOT create PadSetting -- caller must do that. Thread-safe. |
UnassignDevice(Guid) |
Removes ALL UserSettings for a device (all slot assignments). Thread-safe. |
ToggleDeviceSlotAssignment(Guid, int) |
If assigned to the slot, removes. If not, creates. Returns (bool Assigned, UserSetting). Thread-safe. |
GetAssignedSlots(Guid) |
Returns sorted list of all slot indices a device is assigned to. Thread-safe. |
GetSettingsForSlot(int) |
Returns snapshot list of all UserSettings mapped to a pad slot. Thread-safe. |
public static void SwapSlots(int slotA, int slotB)Swaps all persisted slot data between two indices:
- Swaps
SlotCreated[A]andSlotCreated[B] - Swaps
SlotEnabled[A]andSlotEnabled[B] - Updates all
UserSetting.MapTovalues (A->B, B->A) under lock
public static PadSetting CreateDefaultPadSetting(UserDevice ud,
VirtualControllerType outputType = VirtualControllerType.Xbox360)Creates a default PadSetting with auto-mapped inputs. Only auto-maps when ud.CapType == InputDeviceType.Gamepad and ForceRawJoystickMode is not enabled. Non-gamepad devices get an empty PadSetting (user must manually record mappings).
MIDI auto-mapping: When outputType == Midi, maps 6 CCs for axes (MidiCC0-MidiCC5 from Axis 0-Axis 5) and 11 notes for buttons (MidiNote0-MidiNote10 from Button 0-Button 10).
Standardized SDL3 Gamepad Layout (Xbox360/DS4/vJoy):
| Target | Source | Descriptor |
|---|---|---|
| LeftThumbAxisX | SDL Gamepad Axis 0 (LX) | "Axis 0" |
| LeftThumbAxisY | SDL Gamepad Axis 1 (LY) | "Axis 1" |
| LeftTrigger | SDL Gamepad Axis 2 (LT) | "Axis 2" |
| RightThumbAxisX | SDL Gamepad Axis 3 (RX) | "Axis 3" |
| RightThumbAxisY | SDL Gamepad Axis 4 (RY) | "Axis 4" |
| RightTrigger | SDL Gamepad Axis 5 (RT) | "Axis 5" |
| DPadUp / DPadDown / DPadLeft / DPadRight | SDL Gamepad Hat |
"POV 0 Up" / "POV 0 Down" / "POV 0 Left" / "POV 0 Right"
|
| ButtonA | SDL Gamepad Button 0 (A/Cross) | "Button 0" |
| ButtonB | SDL Gamepad Button 1 (B/Circle) | "Button 1" |
| ButtonX | SDL Gamepad Button 2 (X/Square) | "Button 2" |
| ButtonY | SDL Gamepad Button 3 (Y/Triangle) | "Button 3" |
| LeftShoulder | SDL Gamepad Button 4 (LB/L1) | "Button 4" |
| RightShoulder | SDL Gamepad Button 5 (RB/R1) | "Button 5" |
| ButtonBack | SDL Gamepad Button 6 (Back/Share) | "Button 6" |
| ButtonStart | SDL Gamepad Button 7 (Start/Options) | "Button 7" |
| LeftThumbButton | SDL Gamepad Button 8 (LS/L3) | "Button 8" |
| RightThumbButton | SDL Gamepad Button 9 (RS/R3) | "Button 9" |
| ButtonGuide | SDL Gamepad Button 10 (Guide/PS) | "Button 10" |
Default dead zones are set to 0, force feedback to 100%, no motor swap.
public static void ReAutoMapSlot(int padIndex, VirtualControllerType outputType)Re-automaps all devices assigned to a slot for the given output type. Called when switching virtual controller type so mappings match the new layout. Creates a new default PadSetting for each device and updates the UserSetting's checksum.
The save flow is triggered by SettingsService.MarkDirty(), which starts a 250ms debounce timer. When the timer fires, it calls Save(), which calls SaveToFile(filePath):
User action (slider drag, mapping change, etc.)
|
v
SettingsService.MarkDirty()
|-- Sets IsDirty = true
|-- Sets ViewModel.HasUnsavedChanges = true
|-- Starts/restarts 250ms DispatcherTimer
|
... (250ms debounce, timer restarts on each MarkDirty call) ...
|
v
Timer fires -> Save() -> SaveToFile(filePath)
|
v Step 1: UpdatePadSettingsFromViewModels()
| For each PadViewModel (slot 0-15):
| - Find the selected device's UserSetting for this slot
| - Write all ViewModel slider/toggle values back to PadSetting:
| ForceOverall, LeftMotorStrength, RightMotorStrength, ForceSwapMotor,
| AudioRumble settings, dead zones (X/Y), anti-dead zones (X/Y),
| linear, sensitivity curves, max ranges (pos + neg), center offsets,
| trigger dead zones, trigger anti-dead zones, trigger max ranges
| - Write vJoy custom stick/trigger settings for indices 2+ via SetVJoyMapping()
| - Write mapping descriptors via SetPadSettingProperty() (reflection or dict)
|
v Step 2: Flush dictionaries and recompute checksums
| For each UserSetting's PadSetting:
| - FlushVJoyMappings() -- dict -> VJoyMappingEntries[]
| - FlushMidiMappings() -- dict -> MidiMappingEntries[]
| - FlushKbmMappings() -- dict -> KbmMappingEntries[]
| - UpdateChecksum() -- recompute MD5 from all properties
| - Sync: us.PadSettingChecksum = ps.PadSettingChecksum
|
v Step 3: UpdateActiveProfileSnapshot()
| If a named profile is active, write current runtime state back to it:
| entries, PadSettings (deep cloned + deduplicated), slot topology,
| vJoy/MIDI configs, DSU/web server settings
|
v Step 4: Collect data under SyncRoot locks
| data.Devices = UserDevices.Items.ToArray()
| data.Settings = UserSettings.Items.ToArray()
| data.PadSettings = unique PadSettings (deduplicated by checksum via HashSet)
|
v Step 5: Build DTOs
| data.AppSettings = BuildAppSettings() -- from SettingsViewModel
| data.Macros = BuildMacroData() -- from all PadViewModels
| data.Profiles = SettingsManager.Profiles.ToArray()
|
v Step 6: XmlSerializer.Serialize(stream, data)
|
v Step 7: IsDirty = false, raise AutoSaved event
After saving, the AutoSaved event is raised so InputService can refresh the default profile snapshot.
SettingsService.LoadFromFile(string filePath):
LoadFromFile(filePath)
|
v Step 1: XmlSerializer.Deserialize(stream) -> SettingsFileData
|
v Step 2: Populate UserDevices
| Lock UserDevices.SyncRoot, clear, add all devices from XML
|
v Step 3: Populate UserSettings with PadSetting cloning
| Lock UserSettings.SyncRoot, clear, then for each UserSetting:
| - Find matching PadSetting by PadSettingChecksum
| - CLONE it via CloneDeep() (critical: prevents shared mutation)
| - SetPadSetting(clone) on the UserSetting
| - Add to UserSettings.Items
|
v Step 4: Purge orphaned UserSettings
| RemoveAll(us => us.MapTo < 0) -- stale entries from older versions
|
v Step 5: LoadAppSettings(data.AppSettings) -- ORDER MATTERS
| a. Load scalar settings (AutoStartEngine, MinimizeToTray, etc.)
| b. SetLanguageFromCode(appSettings.Language)
| c. Sync EnableAutoProfileSwitching and ActiveProfileId
| d. Load SlotCreated[] (MUST be before OutputType!)
| - If null: AutoCreateSlotsFromExistingAssignments()
| e. Load SlotEnabled[] (defaults to all-true on null)
| f. Load SlotControllerTypes[] (only for created slots)
| g. ApplyVJoyConfigs() and ApplyMidiConfigs()
| h. Load DSU/web server settings
|
v Step 6: LoadPadSettings(data.Settings, data.PadSettings)
| For each UserSetting, first device per slot only:
| - Load force feedback settings to PadViewModel
| - Load dead zones (X/Y), call MigrateAntiDeadZones()
| - Load sensitivity curves, max ranges, call MigrateMaxRangeDirections()
| - Load center offsets, trigger settings
| - SyncAllConfigItemsFromVm()
| - Load vJoy custom stick/trigger settings for indices 2+
| - LoadMappingDescriptors() -- mapping rows from PadSetting
|
v Step 7: LoadMacros(data.Macros)
| Clear all pad macros, reconstruct from serialized data
|
v Step 8: LoadProfiles(data.Profiles, data.AppSettings)
| Always includes built-in "Default" profile at top
| Add all serialized profiles
| If a named profile was active at shutdown:
| - Restore PendingDefaultSnapshot from appSettings.DefaultProfileSnapshot
| - Apply the named profile's topology (SlotCreated, SlotEnabled, types)
| - Apply the named profile's vJoy/MIDI configs
Critical load order: SlotCreated must be loaded before OutputType because setting OutputType fires PropertyChanged, which triggers RefreshNavControllerItems(), which reads SlotCreated[]. Loading out of order causes a double-rebuild crash.
CloneDeep during load is critical: Without cloning, devices that share a checksum would share the same PadSetting object instance. Modifying one device's dead zone would silently corrupt the other device's configuration.
SettingsService.MarkDirty() uses a 250ms debounce DispatcherTimer. Multiple rapid changes (e.g., slider drag) batch into a single save:
MarkDirty() called --> start/restart 250ms timer
MarkDirty() called --> restart 250ms timer
MarkDirty() called --> restart 250ms timer
...250ms passes...
Timer fires --> Save() --> AutoSaved event
Source file: PadForge.Engine/Data/MappingTranslation.cs
When copying mappings between virtual controllers of different types (e.g., Xbox 360 to DS4, vJoy custom to MIDI, gamepad to KB+M), PadForge translates button/axis property names through a canonical positional system rather than copying raw property names.
Each controller layout defines a mapping from its layout-specific property names (e.g., ButtonA, VJoyBtn0, MidiNote60, KbmMBtn0) to a canonical MappingSlot(Category, Position). The category is one of Button, Axis, AxisNeg, or DPad. To translate:
- Look up the source property name in the source layout's table to get a
MappingSlot. - Look up that
MappingSlotin the target layout's table to get the target property name.
For example, copying from Xbox 360 to vJoy custom: ButtonA -> MappingSlot(Button, 0) -> VJoyBtn0.
| Layout | Types | Example Properties |
|---|---|---|
| Gamepad | Xbox 360, DS4, vJoy (gamepad preset) |
ButtonA, LeftThumbAxisX, DPadUp
|
| vJoy Custom | vJoy with custom config |
VJoyBtn0, VJoyAxis0, VJoyPov0Up
|
| MIDI | MIDI |
MidiNote0, MidiCC0
|
| KB+M | Keyboard + Mouse |
KbmMBtn0, KbmMouseX, KbmKey20
|
Xbox 360 and DS4 share the same gamepad property names, so copying between them requires no translation. IsSameLayout() detects this and skips the translation step.
This translation is integrated into the copy/paste workflow on the Pad page. When pasting a mapping from a different controller type, the translation runs automatically so that positionally equivalent controls are matched.
Old settings files have no <SlotCreated> element (null on deserialization). AutoCreateSlotsFromExistingAssignments() scans all UserSettings and creates slots for any MapTo indices that have device assignments:
private static void AutoCreateSlotsFromExistingAssignments()
{
foreach (var us in settings.Items)
{
int idx = us.MapTo;
if (idx >= 0 && idx < InputManager.MaxPads) // MaxPads = 16
{
SettingsManager.SlotCreated[idx] = true;
SettingsManager.SlotEnabled[idx] = true;
}
}
}SlotCreated, SlotEnabled, and SlotControllerTypes arrays use Math.Min(source.Length, target.Length) copy to handle files saved with a different MaxPads value:
int count = Math.Min(appSettings.SlotCreated.Length, SettingsManager.SlotCreated.Length);
Array.Copy(appSettings.SlotCreated, SettingsManager.SlotCreated, count);null on old files defaults to all true (the default value of SettingsManager.SlotEnabled[]).
null on old files defaults to Xbox360 (enum value 0). Uncreated slots are explicitly skipped during type loading to prevent stale values from previous sessions leaking into the engine's SlotControllerTypes array.
null on old files uses the Xbox360 preset defaults for all slots.
null on old files uses the default MIDI configuration for all slots (channel 1, 6 CCs starting at CC 1, 11 notes starting at note 60, velocity 127).
PadSetting.MigrateAntiDeadZones() migrates the legacy unified LeftThumbAntiDeadZone/RightThumbAntiDeadZone properties to per-axis LeftThumbAntiDeadZoneX/Y and RightThumbAntiDeadZoneX/Y. Only migrates when the per-axis values are empty/zero and the unified value is non-zero, ensuring idempotency.
PadSetting.MigrateMaxRangeDirections() copies symmetric max range values to per-direction (negative) properties when the negative-direction property is null/empty. For example, if LeftThumbMaxRangeXNeg is null, it is set to LeftThumbMaxRangeX. This maintains backward compatibility with settings files created before independent per-direction ranges were introduced.
Old profiles without topology data (SlotCreated == null) have topology application skipped during profile switch. The slot layout from the previous profile (or default) is preserved.
On load, UserSettings.Items.RemoveAll(us => us.MapTo < 0) purges entries with MapTo == -1 that may have been left by older versions of the application.