Skip to content

Settings and Serialization

hifihedgehog edited this page Mar 13, 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; }
}

XML Structure

<PadForgeSettings>
  <Devices>
    <Device>
      <InstanceGuid>00000000-0000-0000-0000-000000000000</InstanceGuid>
      <InstanceName>Xbox Controller</InstanceName>
      <ProductGuid>...</ProductGuid>
      <VendorId>1118</VendorId>
      <ProdId>654</ProdId>
      <CapAxeCount>6</CapAxeCount>
      <CapButtonCount>11</CapButtonCount>
      <CapType>4</CapType>
      <HasGyro>false</HasGyro>
      <HasAccel>false</HasAccel>
      ...
    </Device>
  </Devices>
  <UserSettings>
    <Setting>
      <InstanceGuid>...</InstanceGuid>
      <MapTo>0</MapTo>
      <PadSettingChecksum>A1B2C3D4</PadSettingChecksum>
      <IsEnabled>true</IsEnabled>
    </Setting>
  </UserSettings>
  <PadSettings>
    <PadSetting>
      <PadSettingChecksum>A1B2C3D4</PadSettingChecksum>
      <ButtonA>Button 0</ButtonA>
      <LeftThumbAxisX>Axis 0</LeftThumbAxisX>
      <LeftThumbDeadZoneX>0</LeftThumbDeadZoneX>
      ...
    </PadSetting>
  </PadSettings>
  <AppSettings>
    <AutoStartEngine>true</AutoStartEngine>
    <PollingRateMs>1</PollingRateMs>
    <SlotCreated>
      <Created>true</Created>
      <Created>false</Created>
      ...
    </SlotCreated>
    ...
  </AppSettings>
  <Macros>
    <Macro PadIndex="0">
      <Name>Turbo A</Name>
      <TriggerButtons>4096</TriggerButtons>
      <Actions>
        <Action>
          <Type>Button</Type>
          <ButtonFlags>4096</ButtonFlags>
          <DurationMs>50</DurationMs>
        </Action>
      </Actions>
    </Macro>
  </Macros>
  <Profiles>
    <Profile Id="abc123def456" Name="Game Profile">
      <ExecutableNames>C:\Games\game.exe|D:\Other\game2.exe</ExecutableNames>
      <Entries>
        <Entry>
          <InstanceGuid>...</InstanceGuid>
          <MapTo>0</MapTo>
          <PadSettingChecksum>A1B2C3D4</PadSettingChecksum>
        </Entry>
      </Entries>
      <ProfilePadSettings>...</ProfilePadSettings>
      <SlotCreated>...</SlotCreated>
    </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, and DSU settings independently from the root-level data. Switching profiles replaces the runtime state wholesale.


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-7). -1 = unmapped. Fires PropertyChanged for both MapTo and MapToLabel.
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.
SortOrder int <SortOrder> 0 Priority for combining states in Step 4. Lower values = higher priority.
DateCreated DateTime <DateCreated> DateTime.Now When this setting was created.
DateUpdated DateTime <DateUpdated> DateTime.Now When this setting was last modified.
Comment string <Comment> "" Optional user note.
IsAutoMapped bool <IsAutoMapped> false Whether the mapping was auto-generated.

Derived Properties

Property Type Description
MapToLabel string ([XmlIgnore]) Display: "Player 1"-"Player 8" or "Unmapped"

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.
VJoyRawOutputState VJoyRawState Per-device raw vJoy output state for custom vJoy configurations. Only populated when the slot uses vJoy with Custom preset.
_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

[XmlElement]
public string PadSettingChecksum { get; set; } = "";

MD5 hash of all mapping/setting properties, truncated to 8 hex characters (4 bytes). Used to:

  • Link UserSettings to PadSettings
  • Detect duplicates for deduplication during save
  • Identify when a configuration has changed

Computed by ComputeChecksum(), which concatenates all property values (pipe-delimited), includes sorted vJoy mappings for determinism, and takes the first 4 bytes of the MD5 hash as uppercase hex.

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

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

XML Element Type Default Description
LeftThumbCenterOffsetX string "0" Left stick X center offset (-100 to 100%). Raw offset value subtracted before dead zone processing.
LeftThumbCenterOffsetY string "0" Left stick Y center offset
RightThumbCenterOffsetX string "0" Right stick X center offset
RightThumbCenterOffsetY string "0" Right stick Y center offset
LeftThumbMaxRangeX string "100" Left stick X max range (1-100%). Full physical deflection maps to this output ceiling.
LeftThumbMaxRangeY string "100" Left stick Y max range
RightThumbMaxRangeX string "100" Right stick X max range
RightThumbMaxRangeY string "100" Right stick Y max range

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%).
LeftMotorPeriod "0" Motor period in ms. 0 = automatic.
RightMotorPeriod "0" Right motor period in ms.
LeftMotorDirection "0" Motor direction. 0 = normal, 1 = inverted.
RightMotorDirection "0" Right motor direction.

Other Properties

Property Default Description
AxisToButtonThreshold "50" Threshold (0-100%) for treating an axis as a button press. Axis must exceed this percentage to register as pressed.
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.
GameFileName "" Optional game executable name for game-specific settings. Empty = global.

vJoy Custom Mapping Properties

Custom vJoy configurations support arbitrary axis/button/POV counts. Mappings are stored using a dictionary internally (_vjoyMappingDict) with keys like:

  • VJoyAxis0, VJoyAxis0Neg — axis mappings (positive and negative directions)
  • VJoyBtn0, VJoyBtn5 — button mappings
  • VJoyPov0Up, VJoyPov0Down, VJoyPov0Left, VJoyPov0Right — POV directions

The dictionary is serialized to XML as an array of key-value entries:

[XmlArray("VJoyMappings")]
[XmlArrayItem("Map")]
public VJoyMappingEntry[] VJoyMappingEntries { get; set; }
public class VJoyMappingEntry
{
    [XmlAttribute] public string Key { get; set; } = "";
    [XmlAttribute] public string Value { get; set; } = "";
}

Dictionary methods:

Method Description
string GetVJoyMapping(string key) Get descriptor by key. Returns empty string if not found.
void SetVJoyMapping(string key, string value) Set descriptor by key. Empty/null values remove the entry.
void FlushVJoyMappings() Flush dictionary to VJoyMappingEntries array (call before serialization).
void LoadVJoyMappings() Load array into dictionary (called implicitly by EnsureVJoyDict() on first access).

KBM (Keyboard+Mouse) Mapping Properties

Keyboard+Mouse configurations use a dictionary-based mapping system, identical in structure to vJoy mappings. Keys use the "Kbm" prefix:

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

Dictionary methods (same pattern as vJoy):

Method Description
string GetKbmMapping(string key) Get descriptor by key. Returns empty string if not found.
void SetKbmMapping(string key, string value) Set descriptor by key. Empty/null values remove the entry.
void FlushKbmMappings() Flush dictionary to KbmMappingEntries array (call before serialization).

Checksum Computation

public string ComputeChecksum()

Concatenates all mapping, dead zone, force feedback, inversion, threshold, and vJoy mapping properties into a pipe-delimited string. vJoy mappings are sorted by key (StringComparer.Ordinal) for deterministic output. The string is UTF-8 encoded, hashed with MD5, and the first 4 bytes are returned as an 8-character uppercase hex string.

public void UpdateChecksum()

Shorthand that calls ComputeChecksum() and stores the result in PadSettingChecksum.

Utility Methods

Method Signature Description
CloneDeep PadSetting CloneDeep() Deep clone. Copies all properties via CopyFrom(), plus checksum, GameFileName, and deep-copies VJoyMappingEntries[].
CopyFrom void CopyFrom(PadSetting source) Copies all copyable properties from another PadSetting using reflection over CopyablePropertyNames.
ToJson string ToJson() Serializes all copyable properties to a JSON dictionary (for clipboard copy).
FromJson static PadSetting FromJson(string json) Deserializes a JSON dictionary into a new PadSetting (for clipboard paste). Returns null on invalid input.
MigrateAntiDeadZones void MigrateAntiDeadZones() Migrates legacy unified LeftThumbAntiDeadZone/RightThumbAntiDeadZone to per-axis X/Y properties. Only migrates if X/Y are empty/zero and the unified value is non-zero.
HasAnyMapping bool (property) Returns true if at least one mapping property has a non-empty descriptor.

CopyablePropertyNames

The CopyablePropertyNames static array defines which properties participate in CopyFrom(), ToJson(), and FromJson(). It includes: buttons (11), d-pad (5), triggers (8), stick axes (8), dead zones (12), dead zone shapes (2), sensitivity curves (6), stick calibration (8: center offsets + max ranges), force feedback (9), axis inversion (4), and AxisToButtonThreshold. It excludes PadSettingChecksum, GameFileName, and VJoyMappingEntries (vJoy mappings are handled separately via the dictionary).


AppSettingsData

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

public class AppSettingsData
{
    [XmlElement] public bool AutoStartEngine { get; set; } = true;
    [XmlElement] public bool MinimizeToTray { get; set; }
    [XmlElement] public bool StartMinimized { get; set; }
    [XmlElement] public bool StartAtLogin { get; set; }
    [XmlElement] public bool EnablePollingOnFocusLoss { get; set; } = true;
    [XmlElement] public int PollingRateMs { get; set; } = 1;
    [XmlElement] public int ThemeIndex { get; set; }
    [XmlElement] public bool EnableAutoProfileSwitching { get; set; }
    [XmlElement] public string ActiveProfileId { get; set; }

    [XmlArray("SlotControllerTypes")]
    [XmlArrayItem("Type")]
    public int[] SlotControllerTypes { get; set; }

    [XmlArray("SlotCreated")]
    [XmlArrayItem("Created")]
    public bool[] SlotCreated { get; set; }

    [XmlArray("SlotEnabled")]
    [XmlArrayItem("Enabled")]
    public bool[] SlotEnabled { get; set; }

    [XmlElement] public bool EnableDsuMotionServer { get; set; }
    [XmlElement] public int DsuMotionServerPort { get; set; } = 26760;
    [XmlElement] public bool Use2DControllerView { get; set; }

    [XmlArray("VJoyConfigs")]
    [XmlArrayItem("Config")]
    public VJoySlotConfigData[] VJoyConfigs { get; set; }

    [XmlArray("MidiConfigs")]
    [XmlArrayItem("Config")]
    public MidiSlotConfigData[] MidiConfigs { get; set; }

    [XmlArray("HidHideWhitelistPaths")]
    [XmlArrayItem("Path")]
    public string[] HidHideWhitelistPaths { get; set; }
}

Property Details

Property Type Default Description
AutoStartEngine bool true Auto-start the InputManager engine on application launch
MinimizeToTray bool false Minimize to system tray instead of taskbar
StartMinimized bool false Start the application minimized
StartAtLogin bool false Register as a Windows startup application
EnablePollingOnFocusLoss bool true Continue polling when the application loses focus
PollingRateMs int 1 Polling interval in milliseconds (~1000Hz at 1ms)
ThemeIndex int 0 UI theme selection index
EnableAutoProfileSwitching bool false Enable foreground-based automatic profile switching
ActiveProfileId string null ID of the currently active named profile (null = default)
SlotControllerTypes int[] null Per-slot VirtualControllerType enum values (0=Xbox360, 1=DualShock4, 2=VJoy, 3=Midi)
SlotCreated bool[] null Which virtual controller slots are explicitly created
SlotEnabled bool[] null Which slots are enabled for output
EnableDsuMotionServer bool false Enable the DSU/Cemuhook motion server
DsuMotionServerPort int 26760 DSU server listening port
Use2DControllerView bool false Use 2D controller visualization instead of 3D
EnableInputHiding bool true Master switch for all input hiding (HidHide + hooks). When false, no hiding occurs regardless of per-device toggles.
VJoyConfigs VJoySlotConfigData[] null Per-slot vJoy configuration (preset, axis/button/POV counts)
MidiConfigs MidiSlotConfigData[] null Per-slot MIDI configuration (channel, CC/note ranges, velocity)
HidHideWhitelistPaths string[] null Application paths whitelisted in HidHide. Serialized with [XmlArray][XmlArrayItem("Path")]. Null when empty (no element written to XML).

Web Controller Settings

XML Element Type Default Description
EnableWebController bool true Start web controller server
WebControllerPort int 8080 HTTP/WebSocket listen port

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

    [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,...")

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; }
    [XmlElement] public int AxisValue { get; set; }
    [XmlElement] public string AxisTarget { get; set; }
}
Property Type Description
Type MacroActionType Button, Key, Delay, or Axis
ButtonFlags ushort Xbox button flags to press/release
CustomButtons string Hex-encoded vJoy button words
KeyCode int Virtual key code (single key)
KeyString string Multi-key combo in {Key1}{Key2}... format (e.g., {LShiftKey}{A})
DurationMs int How long to hold this action (default: 50ms)
AxisValue int (short range) Axis value for axis actions
AxisTarget string Which axis to target (e.g., LeftThumbX)

ProfileData

Per-application profile. Stores a complete snapshot of device assignments, PadSettings, macros, slot topology, and DSU 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("SlotCreated")]
    [XmlArrayItem("Created")]
    public bool[] SlotCreated { get; set; }

    [XmlArray("SlotEnabled")]
    [XmlArrayItem("Enabled")]
    public bool[] SlotEnabled { get; set; }

    [XmlArray("SlotControllerTypes")]
    [XmlArrayItem("Type")]
    public int[] SlotControllerTypes { get; set; }

    [XmlElement] public bool EnableDsuMotionServer { get; set; }
    [XmlElement] public int DsuMotionServerPort { get; set; }
}
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
Macros MacroData[] Macros for this profile (per-slot)
SlotCreated bool[] Slot topology snapshot
SlotEnabled bool[] Slot enabled states
SlotControllerTypes int[] Per-slot controller types
EnableDsuMotionServer bool DSU server state for this profile
DsuMotionServerPort int DSU server port for this profile

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


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.
EnableAutoProfileSwitching bool Whether auto-switching based on foreground application is enabled.
SlotCreated bool[MaxPads] Which virtual controller slots have been explicitly created. Persisted to settings.
SlotEnabled bool[MaxPads] Which slots are enabled for ViGEm 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

All four 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.
GetOnlineDevices() Returns a snapshot list of online devices (safe to iterate outside lock).
AddOrGetDevice(UserDevice) Adds if not exists (by InstanceGuid), returns existing or new. Thread-safe.
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)

Creates a default PadSetting with auto-mapped inputs. Only auto-maps when ud.CapType == InputDeviceType.Gamepad. Non-gamepad devices get an empty PadSetting (user must manually record mappings).

Standardized SDL3 Gamepad Layout:

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.


Serialization Pipeline

Save Pipeline

SettingsService.SaveToFile(string filePath) performs the following steps:

  1. Flush ViewModel stateUpdatePadSettingsFromViewModels() writes all ViewModel slider values (dead zones, anti-dead zones, force feedback, linear response, mapping descriptors) back to PadSetting objects. For vJoy mappings, uses SetVJoyMapping() instead of reflection.

  2. Flush vJoy dictionaries — For each UserSetting's PadSetting, calls FlushVJoyMappings() to convert the in-memory dictionary to the serializable VJoyMappingEntries[] array.

  3. Recompute checksumsUpdateChecksum() on each PadSetting, then syncs us.PadSettingChecksum = ps.PadSettingChecksum.

  4. Update active profileUpdateActiveProfileSnapshot() writes current runtime state (entries, PadSettings, slot topology, DSU settings) back to the active named profile.

  5. Collect data — Under SyncRoot locks:

    • Devices = snapshot of UserDevices.Items
    • Settings = snapshot of UserSettings.Items
    • PadSettings = deduplicated by checksum (only unique PadSettings are serialized; multiple UserSettings may reference the same checksum)
  6. Build DTOsBuildAppSettings() from SettingsViewModel, BuildMacroData() from all PadViewModels.

  7. SerializeXmlSerializer.Serialize() writes SettingsFileData to disk.

Load Pipeline

SettingsService.LoadFromFile(string filePath):

  1. DeserializeXmlSerializer.Deserialize() reads SettingsFileData.

  2. Populate devices — Locks UserDevices.SyncRoot, clears and adds all devices.

  3. Populate settings with PadSetting cloning — For each UserSetting, finds the matching PadSetting by checksum and clones it (new PadSetting() + CopyFrom(template)). This is critical: without cloning, devices that share a checksum would share the same PadSetting object, so modifying one device's dead zone would silently corrupt the other's.

  4. Purge orphansRemoveAll(us => us.MapTo < 0) removes stale UserSettings from older versions.

  5. Load sub-sections in order:

    • LoadAppSettings()Critical order: SlotCreated before OutputType. Setting OutputType fires PropertyChanged which triggers RefreshNavControllerItems() which reads SlotCreated[]. Loading out of order causes a double-rebuild crash.
    • LoadPadSettings() — First device per slot only; loads dead zones, force feedback, mapping descriptors to PadViewModel.
    • LoadMacros() — Clears all pad macros, reconstructs from serialized data.
    • LoadProfiles() — Always includes built-in "Default" profile at top, then adds serialized profiles.

Autosave

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

After saving, AutoSaved is raised so InputService can refresh the default profile snapshot.


Backward Compatibility

SlotCreated Array (Pre-8-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)
        {
            SettingsManager.SlotCreated[idx] = true;
            SettingsManager.SlotEnabled[idx] = true;
        }
    }
}

Array Size Migration (4-Slot to 8-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.

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