Skip to content

Settings and Serialization

hifihedgehog edited this page Mar 19, 2026 · 65 revisions

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

Table of Contents


PadForge.xml File Format

The settings file is an XML document with SettingsFileData as the root element (serialized as <PadForgeSettings>). The file lives next to the executable.

File Discovery

Search order (in SettingsService.FindSettingsFile()):

  1. PadForge.xml -- preferred for new installs
  2. Settings.xml -- generic fallback for legacy installations
  3. If neither exists, creates PadForge.xml in the application directory

SettingsFileData (Root DTO)

[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; }
}

Complete XML Structure

<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&amp;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>

Key Design Decisions

  1. 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.

  2. 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."

  3. Profiles are self-contained snapshots. Each ProfileData stores its own PadSettings[], Entries[], slot topology, vJoy/MIDI configs, and server settings independently from the root-level data. Switching profiles replaces the runtime state wholesale.

  4. Default profile snapshot preservation. When a named profile is active, AppSettings.DefaultProfileSnapshot stores the default profile's complete state so it can be restored on restart without data loss. The root-level SlotCreated/SlotEnabled/SlotControllerTypes always represent the default profile's state; the active named profile's topology overwrites them at load time.


UserDevice

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.

Serializable Identity Properties

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.

Serializable Capability Properties

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.

Serializable Metadata

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.

Input Hiding

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

Runtime-Only Properties ([XmlIgnore])

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.

Convenience Properties ([XmlIgnore])

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"

Loading Methods

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:

  1. Calls LoadInstance() with identity values (InstanceGuid, Name, ProductGuid)
  2. Calls LoadCapabilities() with capability values (axes, buttons, hats, type)
  3. Sets RawButtonCount = Math.Max(wrapper.RawButtonCount, wrapper.NumButtons)
  4. Sets sensor flags (HasGyro, HasAccel)
  5. Populates VendorId, ProdId, DevicePath, SerialNumber
  6. Builds DeviceObjects and DeviceEffects
  7. Computes AxeMask, ActuatorMask, SliderMask from device objects
  8. Creates ForceFeedbackState if device has rumble or haptic support
  9. Stores the wrapper as Device
public void ClearRuntimeState()

Called on disconnect. Nulls all runtime fields (Device, InputState, DeviceObjects, etc.), sets IsOnline = false, raises NotifyStateChanged().


UserSetting

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.

Serializable Properties

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.

Runtime-Only Properties ([XmlIgnore])

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().

Multi-Slot Assignment Design

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" }

PadSetting

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.

Checksum System

[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() -- What Is Included

ComputeChecksum() builds a pipe-delimited string from all properties that affect the mapping behavior, in a fixed order:

  1. Button mappings (11): ButtonA, ButtonB, ButtonX, ButtonY, LeftShoulder, RightShoulder, ButtonBack, ButtonStart, ButtonGuide, LeftThumbButton, RightThumbButton
  2. D-Pad (5): DPad, DPadUp, DPadDown, DPadLeft, DPadRight
  3. Triggers (8): LeftTrigger, RightTrigger, LeftTriggerDeadZone, RightTriggerDeadZone, LeftTriggerAntiDeadZone, RightTriggerAntiDeadZone, LeftTriggerMaxRange, RightTriggerMaxRange
  4. Thumbstick axes (8): LeftThumbAxisX, LeftThumbAxisY, RightThumbAxisX, RightThumbAxisY, LeftThumbAxisXNeg, LeftThumbAxisYNeg, RightThumbAxisXNeg, RightThumbAxisYNeg
  5. 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
  6. Force feedback (5): ForceType, ForceOverall, ForceSwapMotor, LeftMotorStrength, RightMotorStrength
  7. Audio bass rumble (5): AudioRumbleEnabled, AudioRumbleSensitivity, AudioRumbleCutoffHz, AudioRumbleLeftMotor, AudioRumbleRightMotor
  8. Axis inversion (4): LeftThumbAxisXInvert, LeftThumbAxisYInvert, RightThumbAxisXInvert, RightThumbAxisYInvert
  9. Threshold (1): AxisToButtonThreshold
  10. vJoy custom mappings -- Dictionary entries sorted by key (StringComparer.Ordinal), formatted as key=value|
  11. MIDI custom mappings -- Same sorted key=value format
  12. 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.

Deduplication During Save

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.

Descriptor Format

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).

Complete PadSetting Property Reference

Button Mapping Properties

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)

D-Pad Mapping Properties

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.

Trigger Mapping Properties

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%.

Thumbstick Axis Mapping Properties

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).

Dead Zone / Response Curve Properties

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).

Dead Zone Shape

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).

Sensitivity Curve Properties

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.

Stick Calibration

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.

Force Feedback Properties

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%).

Audio Rumble Properties

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).

Axis Inversion and Threshold

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.

Dictionary-Based Mapping Systems

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.

vJoy Custom Mappings

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 Mappings

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; }

KBM (Keyboard+Mouse) Mappings

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; }

Shared Entry Type

public class VJoyMappingEntry
{
    [XmlAttribute] public string Key { get; set; } = "";
    [XmlAttribute] public string Value { get; set; } = "";
}

Dictionary Methods (same pattern for all three)

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).

CopyablePropertyNames

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 in CopyFrom() and JSON serialization in ToJson()/FromJson()

Usage:

  • CopyFrom(PadSetting source) -- Iterates CopyablePropertyNames via reflection, copies each string property value, then deep-copies the three mapping arrays
  • ToJson() -- Serializes all CopyablePropertyNames plus __VJoyMappings/__MidiMappings/__KbmMappings arrays 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

Utility Methods

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).

Migration Methods

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.

AppSettingsData

File: PadForge.App/Services/SettingsService.cs (inner class)

Application-level settings stored as a single <AppSettings> element inside the root.

Complete Property Reference

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.

Default Profile Snapshot Mechanism

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 (ActiveProfileId is null): slot arrays come from the live runtime state, DefaultProfileSnapshot is null.
  • If a named profile is active: slot arrays come from SettingsManager.PendingDefaultSnapshot (the saved default state), and DefaultProfileSnapshot is set to that snapshot so it survives restart.

During Load (LoadProfiles()):

  • If a named profile was active at shutdown: SettingsManager.PendingDefaultSnapshot is restored from appSettings.DefaultProfileSnapshot. The named profile's topology (SlotCreated, SlotEnabled, SlotControllerTypes, vJoy/MIDI configs) is then applied over the default's loaded values.
  • InputService.Start() uses PendingDefaultSnapshot to initialize _defaultProfileSnapshot correctly.

VJoySlotConfigData

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; }
}

MidiSlotConfigData

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)

MacroData and ActionData

MacroData

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.

ActionData

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

ProfileData

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.

Complete Property Reference

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)

ProfileEntry

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).

Profile Snapshot Mechanism

Profiles are created and updated via the UpdateActiveProfileSnapshot() method, which captures a complete snapshot of the current runtime state:

  1. Entries: Iterates all UserSettings.Items, creating a ProfileEntry for each with InstanceGuid, ProductGuid, MapTo, and PadSettingChecksum.
  2. PadSettings: Collects deep clones (CloneDeep()) of each device's PadSetting, deduplicated by checksum using a HashSet<string>.
  3. Slot topology: Clones SlotCreated and SlotEnabled arrays, collects OutputType from each PadViewModel.
  4. Type-specific configs: Snapshots vJoy configs (only for created vJoy slots) and MIDI configs (only for created MIDI slots).
  5. 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.


SettingsManager

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.

Lifecycle

  1. SettingsService.Initialize() creates the collections and loads from XML.
  2. InputManager.Step1 adds/updates UserDevice records as devices connect/disconnect.
  3. InputService (UI thread) reads collections to sync ViewModels.
  4. SettingsService.Save() serializes collections to XML.

Thread Safety

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
}

Static Properties

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.

Slot Limits

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.

DeviceCollection and SettingsCollection

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) { ... }
}

Device Management Methods

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.

UserSetting Management Methods

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.

Slot Swap

public static void SwapSlots(int slotA, int slotB)

Swaps all persisted slot data between two indices:

  1. Swaps SlotCreated[A] and SlotCreated[B]
  2. Swaps SlotEnabled[A] and SlotEnabled[B]
  3. Updates all UserSetting.MapTo values (A->B, B->A) under lock

Auto-Mapping

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.


Serialization Pipeline

Save Flow (Detailed)

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.

Load Flow (Detailed)

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.

Autosave Debounce

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

Cross-Layout Mapping Translation

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.

How It Works

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:

  1. Look up the source property name in the source layout's table to get a MappingSlot.
  2. Look up that MappingSlot in 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 Groups

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.


Backward Compatibility

SlotCreated Array (Pre-Multi-Slot)

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;
        }
    }
}

Array Size Migration (4-Slot to 16-Slot)

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);

SlotEnabled Default

null on old files defaults to all true (the default value of SettingsManager.SlotEnabled[]).

SlotControllerTypes Default

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.

VJoyConfigs Default

null on old files uses the Xbox360 preset defaults for all slots.

MidiConfigs Default

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).

Anti-Dead Zone Migration

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.

Max Range Direction Migration

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.

Profile Topology

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.

Orphaned UserSettings

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.

Clone this wiki locally