Skip to content

Commit

Permalink
Implement customization of deadzone and saturation properties via con…
Browse files Browse the repository at this point in the history
…figuration file settings.
  • Loading branch information
samuelgr committed May 6, 2023
1 parent 861b043 commit 0d53fec
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 27 deletions.
69 changes: 51 additions & 18 deletions Include/Xidi/Internal/Strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,33 @@
// -------- MACROS --------------------------------------------------------- //

// Strings that need to be available in multiple formats (ASCII and Unicode).
#define XIDI_AXIS_NAME_X "X Axis"
#define XIDI_AXIS_NAME_Y "Y Axis"
#define XIDI_AXIS_NAME_Z "Z Axis"
#define XIDI_AXIS_NAME_RX "RotX Axis"
#define XIDI_AXIS_NAME_RY "RotY Axis"
#define XIDI_AXIS_NAME_RZ "RotZ Axis"
#define XIDI_AXIS_NAME_UNKNOWN "Unknown Axis"
#define XIDI_BUTTON_NAME_FORMAT "Button %u"
#define XIDI_POV_NAME "POV"
#define XIDI_WHOLE_CONTROLLER_NAME "Whole Controller"
#define XIDI_EFFECT_NAME_CONSTANT_FORCE "Constant Force"
#define XIDI_EFFECT_NAME_RAMP_FORCE "Ramp Force"
#define XIDI_EFFECT_NAME_SQUARE "Square Wave"
#define XIDI_EFFECT_NAME_SINE "Sine Wave"
#define XIDI_EFFECT_NAME_TRIANGLE "Triangle Wave"
#define XIDI_EFFECT_NAME_SAWTOOTH_UP "Sawtooth Up"
#define XIDI_EFFECT_NAME_SAWTOOTH_DOWN "Sawtooth Down"
#define XIDI_EFFECT_NAME_CUSTOM_FORCE "Custom Force"
#define XIDI_AXIS_NAME_X "X Axis"
#define XIDI_AXIS_NAME_Y "Y Axis"
#define XIDI_AXIS_NAME_Z "Z Axis"
#define XIDI_AXIS_NAME_RX "RotX Axis"
#define XIDI_AXIS_NAME_RY "RotY Axis"
#define XIDI_AXIS_NAME_RZ "RotZ Axis"
#define XIDI_AXIS_NAME_UNKNOWN "Unknown Axis"
#define XIDI_BUTTON_NAME_FORMAT "Button %u"
#define XIDI_POV_NAME "POV"
#define XIDI_WHOLE_CONTROLLER_NAME "Whole Controller"
#define XIDI_EFFECT_NAME_CONSTANT_FORCE "Constant Force"
#define XIDI_EFFECT_NAME_RAMP_FORCE "Ramp Force"
#define XIDI_EFFECT_NAME_SQUARE "Square Wave"
#define XIDI_EFFECT_NAME_SINE "Sine Wave"
#define XIDI_EFFECT_NAME_TRIANGLE "Triangle Wave"
#define XIDI_EFFECT_NAME_SAWTOOTH_UP "Sawtooth Up"
#define XIDI_EFFECT_NAME_SAWTOOTH_DOWN "Sawtooth Down"
#define XIDI_EFFECT_NAME_CUSTOM_FORCE "Custom Force"

// String prefixes and suffixes that need to be consumed as they are but also combined into longer literals.
// All exist as wide-character strings only.
#define XIDI_CONFIG_PROPERTIES_PREFIX_DEADZONE_PERCENT L"DeadzonePercent"
#define XIDI_CONFIG_PROPERTIES_PREFIX_SATURATION_PERCENT L"SaturationPercent"
#define XIDI_CONFIG_PROPERTIES_SUFFIX_STICK_LEFT L"StickLeft"
#define XIDI_CONFIG_PROPERTIES_SUFFIX_STICK_RIGHT L"StickRight"
#define XIDI_CONFIG_PROPERTIES_SUFFIX_TRIGGER_LT L"TriggerLT"
#define XIDI_CONFIG_PROPERTIES_SUFFIX_TRIGGER_RT L"TriggerRT"


namespace Xidi
Expand Down Expand Up @@ -115,6 +124,30 @@ namespace Xidi
/// Configuration file setting for enabling or disabling built-in properties like deadzone and saturation, which are used for interfaces that do not normally allow for customization.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesUseBuiltinProperties = L"UseBuiltInProperties";

/// Configuration file setting for adding extra deadzone to the left analog stick, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesDeadzonePercentStickLeft = XIDI_CONFIG_PROPERTIES_PREFIX_DEADZONE_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_STICK_LEFT;

/// Configuration file setting for adding extra deadzone to the right analog stick, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesDeadzonePercentStickRight = XIDI_CONFIG_PROPERTIES_PREFIX_DEADZONE_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_STICK_RIGHT;

/// Configuration file setting for adding extra deadzone to the left analog trigger, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesDeadzonePercentTriggerLT = XIDI_CONFIG_PROPERTIES_PREFIX_DEADZONE_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_TRIGGER_LT;

/// Configuration file setting for adding extra deadzone to the right analog trigger, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesDeadzonePercentTriggerRT = XIDI_CONFIG_PROPERTIES_PREFIX_DEADZONE_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_TRIGGER_RT;

/// Configuration file setting for adding extra saturation to the left analog stick, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesSaturationPercentStickLeft = XIDI_CONFIG_PROPERTIES_PREFIX_SATURATION_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_STICK_LEFT;

/// Configuration file setting for adding extra saturation to the right analog stick, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesSaturationPercentStickRight = XIDI_CONFIG_PROPERTIES_PREFIX_SATURATION_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_STICK_RIGHT;

/// Configuration file setting for adding extra saturation to the left analog trigger, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesSaturationPercentTriggerLT = XIDI_CONFIG_PROPERTIES_PREFIX_SATURATION_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_TRIGGER_LT;

/// Configuration file setting for adding extra saturation to the right analog trigger, expressed as a percentage of the analog range.
inline constexpr std::wstring_view kStrConfigurationSettingsPropertiesSaturationPercentTriggerRT = XIDI_CONFIG_PROPERTIES_PREFIX_SATURATION_PERCENT XIDI_CONFIG_PROPERTIES_SUFFIX_TRIGGER_RT;


/// Configuration file section name for specifying behavioral tweaks to work around bugs in games.
inline constexpr std::wstring_view kStrConfigurationSectionWorkarounds = L"Workarounds";
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,16 @@ Type.3 = StandardGamepad
Type.4 = StandardGamepad

[Properties]
; *(unreleased)*
MouseSpeedScalingFactorPercent = 100
UseBuiltInProperties = yes
DeadzonePercentStickLeft = 0
DeadzonePercentStickRight = 0
DeadzonePercentTriggerLT = 0
DeadzonePercentTriggerRT = 0
SaturationPercentStickLeft = 100
SaturationPercentStickRight = 100
SaturationPercentTriggerLT = 100
SaturationPercentTriggerRT = 100

[Log]
Enabled = no
Expand Down Expand Up @@ -166,14 +173,16 @@ This section controls the mapping scheme Xidi uses when mapping between XInput a

## Properties

*(unreleased)*

This section allows for customization and fine-tuning of various virtual controller behaviors, particularly as they pertain to input and output processing.

- **MouseSpeedScalingFactorPercent** modifies the speed of the mouse cursor when a Xidi virtual controller element is configured to emulate mouse movement. Xidi has a built-in default mouse speed, and changing this setting allows that speed to be scaled up or down. The value is expressed as a desired percentage of the default speed. For example, `25` means that the mouse speed should be one-quarter of the default, and `400` means that the mouse speed should be four times the default.

- **UseBuiltInProperties** allows certain built-in axis properties to be enabled or disabled. By default Xidi adds a small deadzone and saturation to all virtual controller axes via WinMM and to any analog sticks or triggers that are used to emulate mouse movement. This is done to ensure a better user experience where such properties are not normally exposed for customization. Setting this to `no` disables these built-in deadzone and saturation properties.

- **DeadzonePercentStickLeft**, **DeadzonePercentStickRight**, **DeadzonePercentTriggerLT**, and **DeadzonePercentTriggerRT** respectively allow the analog deadzone of the left stick, right stick, left trigger, and right trigger to be customized. Deadzone is expressed as percentage of the analog range of motion. If the analog position is less than this percentage away from the neutral position then Xidi reports a neutral reading to the application. It is not generally necessary to customize deadzone because DirectInput applications often use axis properties to do so, and for WinMM, Xidi internally uses its own default properties unless these are disabled. *Any customization done via these configuration file settings is in addition to whatever deadzone the application already sets.*

- **SaturationPercentStickLeft**, **SaturationPercentStickRight**, **SaturationPercentTriggerLT**, and **SaturationPercentTriggerRT** respectively allow the analog saturation of the left stick, right stick, left trigger, and right trigger to be customized. Saturation is expressed as percentage of the analog range of motion. If the analog position is greater than this percentage away from the neutral position then Xidi reports an extreme reading to the application. As with deadzone, it is not generally necessary to customize saturation, and *any customization done via these configuration file settings is in addition to whatever saturation the application already sets.*


## Log

Expand Down
57 changes: 51 additions & 6 deletions Source/Mapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "ApiBitSet.h"
#include "ApiWindows.h"
#include "Configuration.h"
#include "ControllerMath.h"
#include "ControllerTypes.h"
#include "ElementMapper.h"
#include "ForceFeedbackTypes.h"
Expand Down Expand Up @@ -521,27 +522,71 @@ namespace Xidi
};
}

// --------

SState Mapper::MapStatePhysicalToVirtual(SPhysicalState physicalState, uint32_t sourceControllerIdentifier) const
{
// These properties are read from the configuration file and can be used to apply extra transformations to raw analog values read from physical controllers.
// By default, deadzone percentage is set to 0 and saturation percentage is set to 100 to avoid any reduction in full analog range of motion, since most often applications will themselves apply a deadzone and saturation via virtual controller properties.
// However not all applications do this, and some interfaces like WinMM do not even support application-supplied properties.
static const unsigned int kDeadzonePercentStickLeft = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesDeadzonePercentStickLeft).value_or(0);
static const unsigned int kDeadzonePercentStickRight = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesDeadzonePercentStickRight).value_or(0);
static const unsigned int kDeadzonePercentTriggerLT = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesDeadzonePercentTriggerLT).value_or(0);
static const unsigned int kDeadzonePercentTriggerRT = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesDeadzonePercentTriggerRT).value_or(0);
static const unsigned int kSaturationPercentStickLeft = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesSaturationPercentStickLeft).value_or(100);
static const unsigned int kSaturationPercentStickRight = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesSaturationPercentStickRight).value_or(100);
static const unsigned int kSaturationPercentTriggerLT = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesSaturationPercentTriggerLT).value_or(100);
static const unsigned int kSaturationPercentTriggerRT = (unsigned int)Globals::GetConfigurationData().GetFirstIntegerValue(Strings::kStrConfigurationSectionProperties, Strings::kStrConfigurationSettingsPropertiesSaturationPercentTriggerRT).value_or(100);

SState controllerState = {};

// Left and right stick values need to be saturated at the virtual controller range due to a very slight difference between XInput range and virtual controller range.
// This difference (-32768 extreme negative for XInput vs -32767 extreme negative for Xidi) does not affect functionality when filtered by saturation.
// Vertical analog axes additionally need to be inverted because XInput presents up as positive and down as negative whereas Xidi needs to do the opposite.

if (nullptr != elements.named.stickLeftX) elements.named.stickLeftX->ContributeFromAnalogValue(controllerState, FilterAnalogStickValue(physicalState[EPhysicalStick::LeftX]), SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickLeftX)));
if (nullptr != elements.named.stickLeftY) elements.named.stickLeftY->ContributeFromAnalogValue(controllerState, FilterAndInvertAnalogStickValue(physicalState[EPhysicalStick::LeftY]), SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickLeftY)));
if (nullptr != elements.named.stickLeftX)
elements.named.stickLeftX->ContributeFromAnalogValue(
controllerState,
Math::ApplyRawAnalogTransform(FilterAnalogStickValue(physicalState[EPhysicalStick::LeftX]), kDeadzonePercentStickLeft, kSaturationPercentStickLeft),
SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickLeftX))
);
if (nullptr != elements.named.stickLeftY)
elements.named.stickLeftY->ContributeFromAnalogValue(
controllerState,
Math::ApplyRawAnalogTransform(FilterAndInvertAnalogStickValue(physicalState[EPhysicalStick::LeftY]), kDeadzonePercentStickLeft, kSaturationPercentStickLeft),
SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickLeftY))
);

if (nullptr != elements.named.stickRightX) elements.named.stickRightX->ContributeFromAnalogValue(controllerState, FilterAnalogStickValue(physicalState[EPhysicalStick::RightX]), SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickRightX)));
if (nullptr != elements.named.stickRightY) elements.named.stickRightY->ContributeFromAnalogValue(controllerState, FilterAndInvertAnalogStickValue(physicalState[EPhysicalStick::RightY]), SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickRightY)));
if (nullptr != elements.named.stickRightX)
elements.named.stickRightX->ContributeFromAnalogValue(
controllerState,
Math::ApplyRawAnalogTransform(FilterAnalogStickValue(physicalState[EPhysicalStick::RightX]), kDeadzonePercentStickRight, kSaturationPercentStickRight),
SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickRightX))
);
if (nullptr != elements.named.stickRightY)
elements.named.stickRightY->ContributeFromAnalogValue(
controllerState,
Math::ApplyRawAnalogTransform(FilterAndInvertAnalogStickValue(physicalState[EPhysicalStick::RightY]), kDeadzonePercentStickRight, kSaturationPercentStickRight),
SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(stickRightY))
);

if (nullptr != elements.named.dpadUp) elements.named.dpadUp->ContributeFromButtonValue(controllerState, physicalState[EPhysicalButton::DpadUp], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(dpadUp)));
if (nullptr != elements.named.dpadDown) elements.named.dpadDown->ContributeFromButtonValue(controllerState, physicalState[EPhysicalButton::DpadDown], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(dpadDown)));
if (nullptr != elements.named.dpadLeft) elements.named.dpadLeft->ContributeFromButtonValue(controllerState, physicalState[EPhysicalButton::DpadLeft], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(dpadLeft)));
if (nullptr != elements.named.dpadRight) elements.named.dpadRight->ContributeFromButtonValue(controllerState, physicalState[EPhysicalButton::DpadRight], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(dpadRight)));

if (nullptr != elements.named.triggerLT) elements.named.triggerLT->ContributeFromTriggerValue(controllerState, physicalState[EPhysicalTrigger::LT], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(triggerLT)));
if (nullptr != elements.named.triggerRT) elements.named.triggerRT->ContributeFromTriggerValue(controllerState, physicalState[EPhysicalTrigger::RT], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(triggerRT)));
if (nullptr != elements.named.triggerLT)
elements.named.triggerLT->ContributeFromTriggerValue(
controllerState,
Math::ApplyRawTriggerTransform(physicalState[EPhysicalTrigger::LT], kDeadzonePercentTriggerLT, kSaturationPercentTriggerLT),
SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(triggerLT))
);
if (nullptr != elements.named.triggerRT)
elements.named.triggerRT->ContributeFromTriggerValue(
controllerState,
Math::ApplyRawTriggerTransform(physicalState[EPhysicalTrigger::RT], kDeadzonePercentTriggerRT, kSaturationPercentTriggerRT),
SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(triggerRT))
);

if (nullptr != elements.named.buttonA) elements.named.buttonA->ContributeFromButtonValue(controllerState, physicalState[EPhysicalButton::A], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(buttonA)));
if (nullptr != elements.named.buttonB) elements.named.buttonB->ContributeFromButtonValue(controllerState, physicalState[EPhysicalButton::B], SourceIdentifierForElementMapper(sourceControllerIdentifier, ELEMENT_MAP_INDEX_OF(buttonB)));
Expand Down
Loading

0 comments on commit 0d53fec

Please sign in to comment.