-
Notifications
You must be signed in to change notification settings - Fork 6
Settings and Serialization
Developer reference for PadForge's settings persistence layer: XML file format, data models, SettingsManager, and the serialization pipeline.
Source files:
-
PadForge.App/Services/SettingsService.cs-- XML load/save, serialization DTOs -
PadForge.App/Common/SettingsManager.cs-- static thread-safe collections and slot management -
PadForge.Engine/Data/PadSetting.cs-- mapping configuration model (all properties) -
PadForge.Engine/Data/UserDevice.cs-- physical device record -
PadForge.Engine/Data/UserSetting.cs-- device-to-slot linkage
- PadForge.xml File Format
- UserDevice
- UserSetting
- PadSetting
- AppSettingsData
- MacroData and ActionData
- ProfileData
- SettingsManager
- Serialization Pipeline
- Backward Compatibility
The settings file is an XML document with SettingsFileData as the root element (serialized as <PadForgeSettings>). The file lives next to the executable.
Search order (in SettingsService.FindSettingsFile()):
-
PadForge.xml-- preferred for new installs -
Settings.xml-- generic fallback for legacy installations - If neither exists, creates
PadForge.xmlin the application directory
[XmlRoot("PadForgeSettings")]
public class SettingsFileData
{
[XmlArray("Devices")]
[XmlArrayItem("Device")]
public UserDevice[] Devices { get; set; }
[XmlArray("UserSettings")]
[XmlArrayItem("Setting")]
public UserSetting[] Settings { get; set; }
[XmlArray("PadSettings")]
[XmlArrayItem("PadSetting")]
public PadSetting[] PadSettings { get; set; }
[XmlElement("AppSettings")]
public AppSettingsData AppSettings { get; set; }
[XmlArray("Macros")]
[XmlArrayItem("Macro")]
public MacroData[] Macros { get; set; }
[XmlArray("Profiles")]
[XmlArrayItem("Profile")]
public ProfileData[] Profiles { get; set; }
}<PadForgeSettings>
<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>-
PadSettings are deduplicated by checksum. Multiple UserSettings may reference the same
PadSettingChecksum; only one copy of the PadSetting is serialized. This keeps the file small when multiple devices share identical mappings. -
All PadSetting mapping/numeric properties are string-typed. This maintains XML serialization consistency with the original x360ce format and allows empty strings to represent "unmapped."
-
Profiles are self-contained snapshots. Each
ProfileDatastores its ownPadSettings[],Entries[], slot topology, and DSU settings independently from the root-level data. Switching profiles replaces the runtime state wholesale.
File: PadForge.Engine/Data/UserDevice.cs
Namespace: PadForge.Engine.Data
Implements: INotifyPropertyChanged
Represents a single physical input device. Contains both serializable (persisted to XML) properties and runtime-only fields used during the input pipeline.
| Property | Type | XML Element | Description |
|---|---|---|---|
InstanceGuid |
Guid |
<InstanceGuid> |
Deterministic GUID derived from the device's file system path. Unique per USB port + device combination. |
InstanceName |
string |
<InstanceName> |
Human-readable instance name (e.g., "Xbox Controller"). May differ from ProductName. |
ProductGuid |
Guid |
<ProductGuid> |
Product GUID in PIDVID format. Used for fallback matching when instance GUIDs change (e.g., device plugged into a different USB port). |
ProductName |
string |
<ProductName> |
Human-readable product name. |
VendorId |
ushort |
<VendorId> |
USB Vendor ID (e.g., 1118 for Microsoft). |
ProdId |
ushort |
<ProdId> |
USB Product ID. |
DevRevision |
ushort |
<DevRevision> |
USB Product Version / Revision. Only populated for joystick devices (from SdlDeviceWrapper.ProductVersion). |
DevicePath |
string |
<DevicePath> |
File system device path. Used for InstanceGuid generation. |
SerialNumber |
string |
<SerialNumber> |
Device serial number (e.g., Bluetooth MAC address). Empty if unavailable. |
| Property | Type | XML Element | Description |
|---|---|---|---|
CapAxeCount |
int |
<CapAxeCount> |
Number of axes on the device. |
CapButtonCount |
int |
<CapButtonCount> |
Number of buttons (gamepad-mapped count for gamepad devices, which is always 11). |
RawButtonCount |
int |
<RawButtonCount> |
Total raw joystick buttons before gamepad remapping. For gamepad devices, this is higher than CapButtonCount -- it exposes extra native buttons like DualSense touchpad click or mic button. For non-gamepad devices, equals CapButtonCount. |
CapPovCount |
int |
<CapPovCount> |
Number of POV hat switches. |
CapType |
int |
<CapType> |
Device type constant from InputDeviceType (0=Unknown, 1=Mouse, 2=Keyboard, 4=Gamepad, 5=Joystick). |
CapSubType |
int |
<CapSubType> |
Device subtype (not available from SDL, always 0). |
CapFlags |
int |
<CapFlags> |
Capability flags (not available from SDL, always 0). |
HasGyro |
bool |
<HasGyro> |
Whether the device has a gyroscope sensor (e.g., DualSense, Switch Pro Controller). |
HasAccel |
bool |
<HasAccel> |
Whether the device has an accelerometer sensor. |
| Property | Type | XML Element | Description |
|---|---|---|---|
DateCreated |
DateTime |
<DateCreated> |
When this device record was first created (constructor sets to DateTime.Now). |
DateUpdated |
DateTime |
<DateUpdated> |
When this record was last updated (set by LoadInstance() and LoadCapabilities()). |
IsEnabled |
bool |
<IsEnabled> |
Whether this device is enabled for mapping (default: true). |
IsHidden |
bool |
<IsHidden> |
Whether this device is hidden from the UI. Device remains in SettingsManager but is filtered from the device list. |
DisplayName |
string |
<DisplayName> |
User-assigned display name. Overrides InstanceName in the UI when set. |
HidHideEnabled |
bool |
<HidHideEnabled> |
Hide this device from games via HidHide driver (default: false). |
ConsumeInputEnabled |
bool |
<ConsumeInputEnabled> |
Suppress mapped inputs via low-level hooks (default: false). Only meaningful for keyboards and mice. |
| Property | Type | Description |
|---|---|---|
Device |
ISdlInputDevice |
The opened SDL device wrapper. Live handle for state reading and rumble. Set during Step 1, cleared on disconnect. |
IsOnline |
bool |
Whether the device is physically connected and opened. |
InputState |
CustomInputState |
Current input state snapshot. Written by background thread (Step 2), read by UI thread. Reference assignment is atomic. |
InputUpdates |
CustomInputUpdate[] |
Buffered input updates since last poll cycle. |
OldInputState |
CustomInputState |
Previous state for change detection. |
OrgInputState |
CustomInputState |
Original state captured at recording start (for recorder delta detection). |
AxeMask |
int |
Bitmask of present axes (bit N set = axis N exists). |
ActuatorMask |
int |
Bitmask of force-feedback actuator axes. |
ActuatorCount |
int |
Total number of force-feedback actuator axes. |
SliderMask |
int |
Bitmask of present sliders. |
DeviceObjects |
DeviceObjectItem[] |
Axis/button/hat metadata (names, types). Populated during Step 1. |
DeviceEffects |
DeviceEffectItem[] |
Rumble capability metadata. |
ForceFeedbackState |
ForceFeedbackState |
Force feedback / haptic state tracker. Created for devices with rumble or haptic support. |
| Property | Type | Description |
|---|---|---|
IsMouse |
bool |
CapType == InputDeviceType.Mouse |
IsKeyboard |
bool |
CapType == InputDeviceType.Keyboard |
HasForceFeedback |
bool |
ActuatorCount > 0 || Device.HasRumble || Device.HasHaptic |
ResolvedName |
string |
Display name resolution: DisplayName > InstanceName > ProductName > "(Unknown Device)" |
StatusText |
string |
Status string: "Disabled", "Online", or "Offline" |
public void LoadFromSdlDevice(SdlDeviceWrapper wrapper)Main entry point for joystick/gamepad devices. Calls LoadFromDevice() (shared logic) and additionally sets DevRevision.
public void LoadFromKeyboardDevice(SdlKeyboardWrapper wrapper)
public void LoadFromMouseDevice(SdlMouseWrapper wrapper)Entry points for keyboard and mouse. Call LoadFromDevice() directly.
private void LoadFromDevice(ISdlInputDevice wrapper)Shared logic for all device types:
- Calls
LoadInstance()with identity values (InstanceGuid, Name, ProductGuid) - Calls
LoadCapabilities()with capability values (axes, buttons, hats, type) - Sets
RawButtonCount = Math.Max(wrapper.RawButtonCount, wrapper.NumButtons) - Sets sensor flags (
HasGyro,HasAccel) - Populates
VendorId,ProdId,DevicePath,SerialNumber - Builds
DeviceObjectsandDeviceEffects - Computes
AxeMask,ActuatorMask,SliderMaskfrom device objects - Creates
ForceFeedbackStateif device has rumble or haptic support - Stores the wrapper as
Device
public void ClearRuntimeState()Called on disconnect. Nulls all runtime fields (Device, InputState, DeviceObjects, etc.), sets IsOnline = false, raises NotifyStateChanged().
File: PadForge.Engine/Data/UserSetting.cs
Namespace: PadForge.Engine.Data
Implements: INotifyPropertyChanged
Links a physical input device (identified by InstanceGuid) to a virtual controller slot (identified by MapTo) and a mapping configuration (identified by PadSettingChecksum). One UserSetting per device-to-slot assignment.
| Property | Type | XML Element | Default | Description |
|---|---|---|---|---|
InstanceGuid |
Guid |
<InstanceGuid> |
Guid.Empty |
Device instance GUID. Must match UserDevice.InstanceGuid. |
InstanceName |
string |
<InstanceName> |
"" |
Human-readable name at creation time. Used for display when the device is offline. |
ProductGuid |
Guid |
<ProductGuid> |
Guid.Empty |
Product GUID for fallback matching when instance GUIDs change (e.g., different USB port, BT reconnect). |
ProductName |
string |
<ProductName> |
"" |
Human-readable product name. |
MapTo |
int |
<MapTo> |
-1 |
Virtual controller slot index (0-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. |
| Property | Type | Description |
|---|---|---|
MapToLabel |
string ([XmlIgnore]) |
Display: "Player 1"-"Player 8" or "Unmapped" |
| 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(). |
Multiple UserSettings can share the same InstanceGuid with different MapTo values. This enables one physical device to feed multiple virtual controllers simultaneously. Each assignment has its own independent PadSetting (cloned during load to prevent shared-mutation bugs).
Example: A DualSense controller assigned to both Player 1 (slot 0) and Player 3 (slot 2) produces two UserSetting entries:
{ InstanceGuid = "abc...", MapTo = 0, PadSettingChecksum = "X1Y2Z3A4" }{ InstanceGuid = "abc...", MapTo = 2, PadSettingChecksum = "B5C6D7E8" }
File: PadForge.Engine/Data/PadSetting.cs
Namespace: PadForge.Engine.Data
Partial class
Contains the complete mapping configuration for a device-to-slot assignment. All mapping properties are string-typed descriptors in the format consumed by InputManager Step 3.
[XmlElement]
public string PadSettingChecksum { get; set; } = "";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.
All mapping properties use string descriptors understood by the InputManager Step 3 mapping engine:
| Format | Example | Description |
|---|---|---|
"Button N" |
"Button 0" |
Button at index N |
"Axis N" |
"Axis 1" |
Full-range axis at index N |
"IAxis N" |
"IAxis 1" |
Inverted axis (I prefix) |
"HAxis N" |
"HAxis 2" |
Half-axis, 0-100% range (H prefix) |
"IHAxis N" |
"IHAxis 2" |
Inverted half-axis (IH prefix) |
"Slider N" |
"Slider 0" |
Slider at index N |
"POV N Dir" |
"POV 0 Up" |
POV hat at index N, direction: Up, Down, Left, Right, UpRight, UpLeft, DownRight, DownLeft |
"" |
"" |
Unmapped (empty string) |
Prefix meanings:
- I (Invert): Flips the axis direction. Applied by the recorder's auto-inversion logic.
- H (Half): Maps a full-range axis to a 0-100% range (used for triggers and unidirectional mappings).
All [XmlElement], all string, all default to "":
| Property | Description |
|---|---|
ButtonA |
A / Cross button |
ButtonB |
B / Circle button |
ButtonX |
X / Square button |
ButtonY |
Y / Triangle button |
LeftShoulder |
LB / L1 |
RightShoulder |
RB / R1 |
ButtonBack |
Back / Share / Select |
ButtonStart |
Start / Options |
ButtonGuide |
Guide / PS button |
LeftThumbButton |
LS / L3 (left stick press) |
RightThumbButton |
RS / R3 (right stick press) |
| Property | Default | Description |
|---|---|---|
DPad |
"" |
Combined D-Pad mapping. If set to "POV 0", all four directions are auto-extracted by Step 3. |
DPadUp |
"" |
Override: D-Pad up direction |
DPadDown |
"" |
Override: D-Pad down direction |
DPadLeft |
"" |
Override: D-Pad left direction |
DPadRight |
"" |
Override: D-Pad right direction |
When individual DPadUp/Down/Left/Right properties are set, they override the combined DPad property.
| Property | Default | Description |
|---|---|---|
LeftTrigger |
"" |
Left trigger source mapping |
RightTrigger |
"" |
Right trigger source mapping |
LeftTriggerDeadZone |
"0" |
Dead zone 0-100. Values below this percentage are treated as zero. |
RightTriggerDeadZone |
"0" |
Dead zone 0-100. |
LeftTriggerAntiDeadZone |
"0" |
Anti-dead zone 0-100%. Offsets the output range minimum so small physical presses register past the game's built-in dead zone. |
RightTriggerAntiDeadZone |
"0" |
Anti-dead zone 0-100%. |
LeftTriggerMaxRange |
"100" |
Max range 1-100%. Caps the output ceiling so full physical press maps to this percentage. |
RightTriggerMaxRange |
"100" |
Max range 1-100%. |
| Property | Default | Description |
|---|---|---|
LeftThumbAxisX |
"" |
Left stick X positive direction |
LeftThumbAxisY |
"" |
Left stick Y positive direction |
RightThumbAxisX |
"" |
Right stick X positive direction |
RightThumbAxisY |
"" |
Right stick Y positive direction |
LeftThumbAxisXNeg |
"" |
Left stick X negative direction (for button-to-axis mappings) |
LeftThumbAxisYNeg |
"" |
Left stick Y negative direction |
RightThumbAxisXNeg |
"" |
Right stick X negative direction |
RightThumbAxisYNeg |
"" |
Right stick Y negative direction |
The "Neg" variants enable mapping separate physical inputs to opposite directions of a single virtual axis (e.g., mapping two buttons to left/right on a stick).
| Property | Default | Description |
|---|---|---|
LeftThumbDeadZoneX |
"0" |
Left stick dead zone X axis (0-100%) |
LeftThumbDeadZoneY |
"0" |
Left stick dead zone Y axis (0-100%) |
RightThumbDeadZoneX |
"0" |
Right stick dead zone X axis (0-100%) |
RightThumbDeadZoneY |
"0" |
Right stick dead zone Y axis (0-100%) |
LeftThumbAntiDeadZone |
"0" |
Legacy unified anti-dead zone (migrated to per-axis on load) |
RightThumbAntiDeadZone |
"0" |
Legacy unified anti-dead zone |
LeftThumbAntiDeadZoneX |
"0" |
Left stick anti-dead zone X (0-100%) |
LeftThumbAntiDeadZoneY |
"0" |
Left stick anti-dead zone Y (0-100%) |
RightThumbAntiDeadZoneX |
"0" |
Right stick anti-dead zone X (0-100%) |
RightThumbAntiDeadZoneY |
"0" |
Right stick anti-dead zone Y (0-100%) |
LeftThumbLinear |
"0" |
Left stick linear response curve (0-100). 0 = default curve, 100 = fully linear. |
RightThumbLinear |
"0" |
Right stick linear response curve (0-100). |
| Property | Default | Description |
|---|---|---|
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. |
| 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. |
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). |
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.
| 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. |
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), force feedback (9), axis inversion (4), and AxisToButtonThreshold. It excludes PadSettingChecksum, GameFileName, and VJoyMappingEntries (vJoy mappings are handled separately via the dictionary).
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; }
}| 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) |
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) |
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; }
}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,...") |
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) |
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 |
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).
File: PadForge.App/Common/SettingsManager.cs
Namespace: PadForge.Common.Input
Static partial class (canonical partial in SettingsManager.cs; additional partial declarations in InputManager.Step1.UpdateDevices.cs for UserDevices/UserSettings property declarations and the DeviceCollection/SettingsCollection class definitions)
Central manager for device records and mapping settings. Shared between the background engine thread and the UI thread.
-
SettingsService.Initialize()creates the collections and loads from XML. -
InputManager.Step1adds/updates UserDevice records as devices connect/disconnect. -
InputService(UI thread) reads collections to sync ViewModels. -
SettingsService.Save()serializes collections to XML.
All access to UserDevices and UserSettings must be done inside a lock on the respective collection's SyncRoot:
lock (SettingsManager.UserDevices.SyncRoot)
{
// Safe to iterate/modify UserDevices.Items
}| Property | Type | Description |
|---|---|---|
UserDevices |
DeviceCollection |
All known physical devices. Declared in InputManager.Step1.UpdateDevices.cs. |
UserSettings |
SettingsCollection |
All device-to-slot assignments. Declared in InputManager.Step1.UpdateDevices.cs. |
Profiles |
List<ProfileData> |
All saved profiles. Empty list = no profiles configured. |
ActiveProfileId |
string |
ID of the currently active named profile, or null for the default profile. |
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. |
| 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 |
All three types share the same global limit of 16. The "Add Controller" button disappears when all 16 slots are in use.
Defined in InputManager.Step1.UpdateDevices.cs:
public class DeviceCollection
{
public List<UserDevice> Items { get; } = new();
public object SyncRoot { get; } = new();
}
public class SettingsCollection
{
public List<UserSetting> Items { get; } = new();
public object SyncRoot { get; } = new();
public UserSetting FindByInstanceGuid(Guid guid) { ... }
public List<UserSetting> FindByPadIndex(int padIndex) { ... }
}| Method | Description |
|---|---|
EnsureInitialized() |
Creates collections if null. Safe to call multiple times. |
FindDeviceByInstanceGuid(Guid) |
Thread-safe lookup. Returns null if not found. |
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. |
| Method | Description |
|---|---|
FindSettingByInstanceGuid(Guid) |
Find first UserSetting for a device. Thread-safe. |
FindSettingByInstanceGuidAndSlot(Guid, int) |
Find UserSetting for a specific device+slot pair. Required for multi-slot devices. Thread-safe. |
AssignDeviceToSlot(Guid, int) |
Creates or returns existing UserSetting. Supports multi-slot: creates a new entry for each additional slot. Does NOT create PadSetting -- caller must do that. Thread-safe. |
UnassignDevice(Guid) |
Removes ALL UserSettings for a device (all slot assignments). Thread-safe. |
ToggleDeviceSlotAssignment(Guid, int) |
If assigned to the slot, removes. If not, creates. Returns (bool Assigned, UserSetting). Thread-safe. |
GetAssignedSlots(Guid) |
Returns sorted list of all slot indices a device is assigned to. Thread-safe. |
GetSettingsForSlot(int) |
Returns snapshot list of all UserSettings mapped to a pad slot. Thread-safe. |
public static void SwapSlots(int slotA, int slotB)Swaps all persisted slot data between two indices:
- Swaps
SlotCreated[A]andSlotCreated[B] - Swaps
SlotEnabled[A]andSlotEnabled[B] - Updates all
UserSetting.MapTovalues (A->B, B->A) under lock
public static PadSetting CreateDefaultPadSetting(UserDevice ud)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.
SettingsService.SaveToFile(string filePath) performs the following steps:
-
Flush ViewModel state --
UpdatePadSettingsFromViewModels()writes all ViewModel slider values (dead zones, anti-dead zones, force feedback, linear response, mapping descriptors) back toPadSettingobjects. For vJoy mappings, usesSetVJoyMapping()instead of reflection. -
Flush vJoy dictionaries -- For each UserSetting's PadSetting, calls
FlushVJoyMappings()to convert the in-memory dictionary to the serializableVJoyMappingEntries[]array. -
Recompute checksums --
UpdateChecksum()on each PadSetting, then syncsus.PadSettingChecksum = ps.PadSettingChecksum. -
Update active profile --
UpdateActiveProfileSnapshot()writes current runtime state (entries, PadSettings, slot topology, DSU settings) back to the active named profile. -
Collect data -- Under
SyncRootlocks:-
Devices= snapshot ofUserDevices.Items -
Settings= snapshot ofUserSettings.Items -
PadSettings= deduplicated by checksum (only unique PadSettings are serialized; multiple UserSettings may reference the same checksum)
-
-
Build DTOs --
BuildAppSettings()from SettingsViewModel,BuildMacroData()from all PadViewModels. -
Serialize --
XmlSerializer.Serialize()writesSettingsFileDatato disk.
SettingsService.LoadFromFile(string filePath):
-
Deserialize --
XmlSerializer.Deserialize()readsSettingsFileData. -
Populate devices -- Locks
UserDevices.SyncRoot, clears and adds all devices. -
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 samePadSettingobject, so modifying one device's dead zone would silently corrupt the other's. -
Purge orphans --
RemoveAll(us => us.MapTo < 0)removes stale UserSettings from older versions. -
Load sub-sections in order:
-
LoadAppSettings()-- Critical order:SlotCreatedbeforeOutputType. SettingOutputTypefiresPropertyChangedwhich triggersRefreshNavControllerItems()which readsSlotCreated[]. 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.
-
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.
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;
}
}
}SlotCreated, SlotEnabled, and SlotControllerTypes arrays use Math.Min(source.Length, target.Length) copy to handle files saved with a different MaxPads value:
int count = Math.Min(appSettings.SlotCreated.Length, SettingsManager.SlotCreated.Length);
Array.Copy(appSettings.SlotCreated, SettingsManager.SlotCreated, count);null on old files defaults to all true (the default value of SettingsManager.SlotEnabled[]).
null on old files defaults to Xbox360 (enum value 0). Uncreated slots are explicitly skipped during type loading to prevent stale values from previous sessions leaking into the engine's SlotControllerTypes array.
null on old files uses the Xbox360 preset defaults for all slots.
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.
Old profiles without topology data (SlotCreated == null) have topology application skipped during profile switch. The slot layout from the previous profile (or default) is preserved.
On load, UserSettings.Items.RemoveAll(us => us.MapTo < 0) purges entries with MapTo == -1 that may have been left by older versions of the application.