From 14234b1337fed18786ecb36ee06bb2ba09ec4d51 Mon Sep 17 00:00:00 2001 From: Samuel Grossman Date: Sat, 5 Nov 2022 14:42:04 -0700 Subject: [PATCH] Completed implementation and testing of mouse axis mapper parsing from a configuration file. Updated documentation. --- DEVELOPERS.md | 9 +- README.md | 30 +++- Source/MapperParser.cpp | 153 ++++++++++-------- .../Test/Case/ForceFeedbackParametersTest.cpp | 2 +- Source/Test/Case/MapperParserTest.cpp | 24 ++- 5 files changed, 143 insertions(+), 75 deletions(-) diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 10666cc0..a840ed6c 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -95,6 +95,13 @@ Behavior is very similar to ButtonMapper in terms of the logic. However, instead This type of mapper is considered to have a side effect because, unlike other types, it does not not contribute directly to virtual controller state but rather to the keyboard state. As a result it implements `ContributeNeutral` so that associated keyboard buttons can be released in the absence of input from the controller. +##### MouseAxisMapper + +Behavior and implementation is similar to AxisMapper and DigitalAxisMapper. However, instead of a virtual controller axis, this type of element mapper simulates motion of the system mouse along one of the four supported axes (X, Y, horizontal mouse wheel, vertical mouse wheel). + +This mapper uses an opaque source identifier to allow the Mouse module to aggregate across all possible sources of mouse input for the purpose of determining the net contribution Xidi will make at any given time to movement of the system mouse. Unlike virtual controller motion, mouse movement is always relative. + + ##### MouseButtonMapper Behavior and implementation is extremely similar to KeyboardMapper. However, instead of a virtual keyboard key, this type of element mapper simulates a mouse button press on the system mouse. @@ -196,7 +203,7 @@ Source code documentation is available and can be built using Doxygen. This sect **MapperParser** implements all string-parsing functionality for identifying XInput controller elements, identifying force feedback actuators, and constructing both of these types of objects based on strings contained within a configuration file. -**Mouse** tracks virtual mouse state as reported by any `MouseButtonMapper` objects that may exist. It maintains state information for each possible mouse button and periodically submits mouse events to the system using the `SendInput` Windows API function. +**Mouse** tracks virtual mouse state as reported by any `MouseAxisMapper` and `MouseButtonMapper` objects that may exist. It maintains state information for each possible mouse axis and button, periodically submitting mouse events to the system using the `SendInput` Windows API function. For mouse axes, this module keeps track of movement contributions from all physical sources, aggregates across them using summation, and appropriately converts from the absolute position scheme reported by game controllers to the relative motion scheme that Windows uses for mouse movement. **PhysicalController** manages all communication with the underlying XInput API. It periodically polls devices for changes to physical state and supports notifying other modules whenever a physical state change is detected. diff --git a/README.md b/README.md index 67d08f5c..ec3da923 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The remainder of this document is organized as follows. # Getting Started -1. Ensure the system is running Windows 10 or 11. Xidi is built to target Windows 10 or 11 and does not support older versions of Windows. +1. Ensure the system is running Windows 10 or 11. Xidi is built to target Windows 10 or 11 and may not run correctly on older versions of Windows. 1. Ensure the [Visual C++ Runtime for Visual Studio 2022](https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) is installed. Xidi is linked against this runtime and will not work without it. If running a 64-bit operating system, install both the x86 and the x64 versions of this runtime, otherwise install just the x86 version. @@ -517,7 +517,33 @@ ButtonBack = Keyboard(Esc) ``` -#### MouseButton *(unreleased)* +#### MouseAxis + +A mouse axis element mapper links an XInput controller element to mouse movement along one of the possible mouse axes. It otherwise behaves very similarly to an axis mapper. + +MouseAxis requires a parameter specifying the mouse motion axis to which to link. Supported axis names are `X`, `Y`, `WheelHorizontal`, `WheelVertical`; the first two correspond to physical mouse movement, and the second two correspond to rotation of the scroll wheel that is present on some mouse hardware. A second optional parameter is additionally allowed to specify the axis direction, either `+` or `-` (alternative values `Positive` and `Negative` are also accepted). + +As an example, the below configuration links the right stick and the d-pad to mouse cursor movement. + +```ini +[CustomMapper:MouseAxisExample] + +; This example is not complete. +; It only defines element mappers for a small subset of controller elements. + +; For the right stick one mouse axis maps entirely to an analog controller axis. +StickRightX = MouseAxis(X) +StickRightY = MouseAxis(Y) + +; For the d-pad one button corresponds to a different direction of motion. +DpadUp = MouseAxis(Y, -) +DpadDown = MouseAxis(Y, +) +DpadLeft = MouseAxis(X, -) +DpadRight = MouseAxis(X, +) +``` + + +#### MouseButton A mouse button element mapper links an XInput controller element to a mouse button. It behaves very similarly to a keyboard element mapper except it acts on a mouse button rather than on a keyboard key. diff --git a/Source/MapperParser.cpp b/Source/MapperParser.cpp index 84e9ebac..5c966af4 100644 --- a/Source/MapperParser.cpp +++ b/Source/MapperParser.cpp @@ -69,25 +69,79 @@ namespace Xidi /// Type for all functions that attempt to build force feedback actuator description objects given a parameter string. typedef ForceFeedbackActuatorOrError(*TMakeForceFeedbackActuatorFunc)(std::wstring_view); - /// Holds parameters for creating #AxisMapper objects. - /// Useful because both #AxisMapper and #DigitalAxisMapper follow the same parsing logic and parameters. - /// See #AxisMapper for documentation on the fields. - struct SAxisMapperParams + /// Holds parameters for creating various types of axis mapper objects, where those mapper objects include an axis enumerator and an axis direction enumerator. + /// @tparam AxisEnumType Axis enumeration type that identifies the target axis. + template struct SAxisParams { - EAxis axis; + AxisEnumType axis; EAxisDirection direction; }; /// Type alias for enabling axis parameter parsing to indicate a semantically-rich error on parse failure. - typedef ValueOrError AxisMapperParamsOrError; + /// @tparam AxisEnumType Axis enumeration type that identifies the target axis. + template using AxisParamsOrError = ValueOrError, std::wstring>; // -------- INTERNAL FUNCTIONS --------------------------------- // - /// Attempts to map a string to an axis type enumerator. + /// Attempts to map a string to an axis direction enumerator. + /// @param [in] directionString String supposedly representing an axis direction. + /// @return Corresponding axis direction enumerator, if the string is parseable as such. + static std::optional AxisDirectionFromString(std::wstring_view directionString) + { + // Map of strings representing axis directions to axis direction enumerators. + static const std::map kDirectionStrings = { + {L"bidir", EAxisDirection::Both}, + {L"Bidir", EAxisDirection::Both}, + {L"BiDir", EAxisDirection::Both}, + {L"BIDIR", EAxisDirection::Both}, + {L"bidirectional", EAxisDirection::Both}, + {L"Bidirectional", EAxisDirection::Both}, + {L"BiDirectional", EAxisDirection::Both}, + {L"BIDIRECTIONAL", EAxisDirection::Both}, + {L"both", EAxisDirection::Both}, + {L"Both", EAxisDirection::Both}, + {L"BOTH", EAxisDirection::Both}, + + {L"+", EAxisDirection::Positive}, + {L"+ve", EAxisDirection::Positive}, + {L"pos", EAxisDirection::Positive}, + {L"Pos", EAxisDirection::Positive}, + {L"POS", EAxisDirection::Positive}, + {L"positive", EAxisDirection::Positive}, + {L"Positive", EAxisDirection::Positive}, + {L"POSITIVE", EAxisDirection::Positive}, + + {L"-", EAxisDirection::Negative}, + {L"-ve", EAxisDirection::Negative}, + {L"neg", EAxisDirection::Negative}, + {L"Neg", EAxisDirection::Negative}, + {L"NEG", EAxisDirection::Negative}, + {L"negative", EAxisDirection::Negative}, + {L"Negative", EAxisDirection::Negative}, + {L"NEGATIVE", EAxisDirection::Negative} + }; + + const auto kDirectionIter = kDirectionStrings.find(directionString); + if (kDirectionStrings.cend() == kDirectionIter) + return std::nullopt; + + return kDirectionIter->second; + } + + /// Attempts to map a string to an axis type enumerator. This generic version does nothing. + /// @tparam AxisEnumType Axis enumeration type that identifies the target axis. + /// @param [in] axisString String supposedly representing an axis type. + /// @return Corresponding axis type enumerator, if the string is parseable as such. + template static std::optional AxisTypeFromString(std::wstring_view axisString) + { + return std::nullopt; + } + + /// Attempts to map a string to an axis type enumerator, specialized for analog axes. /// @param [in] axisString String supposedly representing an axis type. /// @return Corresponding axis type enumerator, if the string is parseable as such. - static std::optional AxisFromString(std::wstring_view axisString) + template <> static std::optional AxisTypeFromString(std::wstring_view axisString) { // Map of strings representing axes to axis enumerators. static const std::map kAxisStrings = { @@ -135,55 +189,10 @@ namespace Xidi return kAxisIter->second; } - /// Attempts to map a string to an axis direction enumerator. - /// @param [in] directionString String supposedly representing an axis direction. - /// @return Corresponding axis direction enumerator, if the string is parseable as such. - static std::optional AxisDirectionFromString(std::wstring_view directionString) - { - // Map of strings representing axis directions to axis direction enumerators. - static const std::map kDirectionStrings = { - {L"bidir", EAxisDirection::Both}, - {L"Bidir", EAxisDirection::Both}, - {L"BiDir", EAxisDirection::Both}, - {L"BIDIR", EAxisDirection::Both}, - {L"bidirectional", EAxisDirection::Both}, - {L"Bidirectional", EAxisDirection::Both}, - {L"BiDirectional", EAxisDirection::Both}, - {L"BIDIRECTIONAL", EAxisDirection::Both}, - {L"both", EAxisDirection::Both}, - {L"Both", EAxisDirection::Both}, - {L"BOTH", EAxisDirection::Both}, - - {L"+", EAxisDirection::Positive}, - {L"+ve", EAxisDirection::Positive}, - {L"pos", EAxisDirection::Positive}, - {L"Pos", EAxisDirection::Positive}, - {L"POS", EAxisDirection::Positive}, - {L"positive", EAxisDirection::Positive}, - {L"Positive", EAxisDirection::Positive}, - {L"POSITIVE", EAxisDirection::Positive}, - - {L"-", EAxisDirection::Negative}, - {L"-ve", EAxisDirection::Negative}, - {L"neg", EAxisDirection::Negative}, - {L"Neg", EAxisDirection::Negative}, - {L"NEG", EAxisDirection::Negative}, - {L"negative", EAxisDirection::Negative}, - {L"Negative", EAxisDirection::Negative}, - {L"NEGATIVE", EAxisDirection::Negative} - }; - - const auto kDirectionIter = kDirectionStrings.find(directionString); - if (kDirectionStrings.cend() == kDirectionIter) - return std::nullopt; - - return kDirectionIter->second; - } - - /// Attempts to map a string to a mouse axis type enumerator. - /// @param [in] mouseAxisString String supposedly representing an axis type. - /// @return Corresponding mouse axis type enumerator, if the string is parseable as such. - static std::optional MouseAxisFromString(std::wstring_view mouseAxisString) + /// Attempts to map a string to an axis type enumerator, specialized for mouse axes. + /// @param [in] axisString String supposedly representing an axis type. + /// @return Corresponding axis type enumerator, if the string is parseable as such. + template <> static std::optional AxisTypeFromString(std::wstring_view axisString) { // Map of strings representing mouse axes to mouse axis enumerators. static const std::map kMouseAxisStrings = { @@ -224,7 +233,7 @@ namespace Xidi {L"WheelVertical", Mouse::EMouseAxis::WheelVertical} }; - const auto kMouseAxisIter = kMouseAxisStrings.find(mouseAxisString); + const auto kMouseAxisIter = kMouseAxisStrings.find(axisString); if (kMouseAxisStrings.cend() == kMouseAxisIter) return std::nullopt; @@ -300,11 +309,11 @@ namespace Xidi return std::wstring_view::npos; } - /// Common logic for parsing axis mapper parameters from an axis mapper string. - /// Used for creating both #AxisMapper and #DigitalAxisMapper objects. + /// Common logic for parsing various types of axis mapper parameters from an axis mapper string. + /// @tparam AxisEnumType Axis enumeration type that identifies the target axis. /// @param [in] params Parameter string. /// @return Structure containing the parsed parameters if parsing was successful, error message otherwise. - static AxisMapperParamsOrError ParseAxisMapperParams(std::wstring_view params) + template static AxisParamsOrError ParseAxisParams(std::wstring_view params) { SParamStringParts paramParts = ExtractParameterListStringParts(params).value_or(SParamStringParts()); @@ -312,11 +321,11 @@ namespace Xidi if (true == paramParts.first.empty()) return L"Missing or unparseable axis"; - const std::optional kMaybeAxis = AxisFromString(paramParts.first); + const std::optional kMaybeAxis = AxisTypeFromString(paramParts.first); if (false == kMaybeAxis.has_value()) return Strings::FormatString(L"%s: Unrecognized axis", std::wstring(paramParts.first).c_str()).Data(); - const EAxis kAxis = kMaybeAxis.value(); + const AxisEnumType kAxis = kMaybeAxis.value(); // Second parameter is optional. It is a string that specifies the axis direction, with the default being both. EAxisDirection axisDirection = EAxisDirection::Both; @@ -336,7 +345,7 @@ namespace Xidi if (false == paramParts.remaining.empty()) return Strings::FormatString(L"\"%s\" is extraneous", std::wstring(paramParts.remaining).c_str()).Data(); - return SAxisMapperParams({.axis = kAxis, .direction = axisDirection}); + return SAxisParams({.axis = kAxis, .direction = axisDirection}); } /// Parses a relatively small unsigned integer value from the supplied input string. @@ -945,7 +954,7 @@ namespace Xidi ElementMapperOrError MakeAxisMapper(std::wstring_view params) { - const AxisMapperParamsOrError kMaybeAxisMapperParams = ParseAxisMapperParams(params); + const AxisParamsOrError kMaybeAxisMapperParams = ParseAxisParams(params); if (true == kMaybeAxisMapperParams.HasError()) return Strings::FormatString(L"Axis: %s", kMaybeAxisMapperParams.Error().c_str()).Data(); @@ -1000,7 +1009,7 @@ namespace Xidi ElementMapperOrError MakeDigitalAxisMapper(std::wstring_view params) { - const AxisMapperParamsOrError kMaybeAxisMapperParams = ParseAxisMapperParams(params); + const AxisParamsOrError kMaybeAxisMapperParams = ParseAxisParams(params); if (true == kMaybeAxisMapperParams.HasError()) return Strings::FormatString(L"DigitalAxis: %s", kMaybeAxisMapperParams.Error().c_str()).Data(); @@ -1045,7 +1054,11 @@ namespace Xidi ElementMapperOrError MakeMouseAxisMapper(std::wstring_view params) { - return L"MouseAxis: Not yet implemented."; + const AxisParamsOrError kMaybeMouseAxisMapperParams = ParseAxisParams(params); + if (true == kMaybeMouseAxisMapperParams.HasError()) + return Strings::FormatString(L"MouseAxis: %s", kMaybeMouseAxisMapperParams.Error().c_str()).Data(); + + return std::make_unique(kMaybeMouseAxisMapperParams.Value().axis, kMaybeMouseAxisMapperParams.Value().direction); } // -------- @@ -1166,7 +1179,7 @@ namespace Xidi ForceFeedbackActuatorOrError MakeForceFeedbackActuatorSingleAxis(std::wstring_view params) { - const AxisMapperParamsOrError kMaybeAxisMapperParams = ParseAxisMapperParams(params); + const AxisParamsOrError kMaybeAxisMapperParams = ParseAxisParams(params); if (true == kMaybeAxisMapperParams.HasError()) return Strings::FormatString(L"SingleAxis: %s", kMaybeAxisMapperParams.Error().c_str()).Data(); @@ -1192,7 +1205,7 @@ namespace Xidi if (true == paramParts.first.empty()) return L"MagnitudeProjection: Missing or unparseable first axis"; - const std::optional kMaybeAxisFirst = AxisFromString(paramParts.first); + const std::optional kMaybeAxisFirst = AxisTypeFromString(paramParts.first); if (false == kMaybeAxisFirst.has_value()) return Strings::FormatString(L"MagnitudeProjection: %s: Unrecognized first axis", std::wstring(paramParts.first).c_str()).Data(); @@ -1203,7 +1216,7 @@ namespace Xidi if (true == paramParts.first.empty()) return L"MagnitudeProjection: Missing or unparseable second axis"; - const std::optional kMaybeAxisSecond = AxisFromString(paramParts.first); + const std::optional kMaybeAxisSecond = AxisTypeFromString(paramParts.first); if (false == kMaybeAxisSecond.has_value()) return Strings::FormatString(L"MagnitudeProjection: %s: Unrecognized second axis", std::wstring(paramParts.first).c_str()).Data(); diff --git a/Source/Test/Case/ForceFeedbackParametersTest.cpp b/Source/Test/Case/ForceFeedbackParametersTest.cpp index 747d45e0..b0857be3 100644 --- a/Source/Test/Case/ForceFeedbackParametersTest.cpp +++ b/Source/Test/Case/ForceFeedbackParametersTest.cpp @@ -102,7 +102,7 @@ namespace XidiTest /// @param [in] valueA First of the two magnitude component vectors to compare. /// @param [in] valueB Second of the two magnitude component to compare. /// @return `true` if the two magnitude component vectors all have components that are approximately equal, `false` otherwise. - template<> static bool ApproximatelyEqual(TMagnitudeComponents valueA, TMagnitudeComponents valueB) + template <> static bool ApproximatelyEqual(TMagnitudeComponents valueA, TMagnitudeComponents valueB) { for (size_t i = 0; i < valueA.size(); ++i) { diff --git a/Source/Test/Case/MapperParserTest.cpp b/Source/Test/Case/MapperParserTest.cpp index c5e3391d..24992f08 100644 --- a/Source/Test/Case/MapperParserTest.cpp +++ b/Source/Test/Case/MapperParserTest.cpp @@ -559,7 +559,29 @@ namespace XidiTest // Verifies correct construction of mouse axis mapper objects in the nominal case of valid parameter strings being passed. TEST_CASE(MapperParser_MakeMouseAxisMapper_Nominal) { - // TODO + constexpr struct { + std::wstring_view params; + EMouseAxis axis; + EAxisDirection axisDirection; + } kMouseAxisMapperTestItems[] = { + {.params = L" X ", .axis = EMouseAxis::X}, + {.params = L" Horizontal, -", .axis = EMouseAxis::X, .axisDirection = EAxisDirection::Negative}, + {.params = L"WheelVertical, +", .axis = EMouseAxis::WheelVertical, .axisDirection = EAxisDirection::Positive} + }; + + for (auto& mouseAxisMapperTestItem : kMouseAxisMapperTestItems) + { + ElementMapperOrError maybeMouseAxisMapper = MapperParser::MakeMouseAxisMapper(mouseAxisMapperTestItem.params); + + TEST_ASSERT(true == maybeMouseAxisMapper.HasValue()); + TEST_ASSERT(0 == maybeMouseAxisMapper.Value()->GetTargetElementCount()); + + const MouseAxisMapper* const mouseAxisMapper = dynamic_cast(maybeMouseAxisMapper.Value().get()); + TEST_ASSERT(nullptr != mouseAxisMapper); + + TEST_ASSERT(mouseAxisMapperTestItem.axis == mouseAxisMapper->GetAxis()); + TEST_ASSERT(mouseAxisMapperTestItem.axisDirection == mouseAxisMapper->GetAxisDirection()); + } } // Verifies correct failure to create mouse button mapper objects when the parameter strings are invalid.