diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c358ee..4a8a2ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,26 +9,8 @@ if (POLICY CMP0141) set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$,$>,$<$:EditAndContinue>,$<$:ProgramDatabase>>") endif() -project ("StreamToActionTranslator" CXX) - - -# Add source to this project's executable. -#add_executable (StreamToActionTranslator "src/StreamToActionTranslator.h" "src/main.cpp") -add_library (StreamToActionTranslator OBJECT "src/StreamToActionTranslator.h" ) - -set(TARGET StreamToActionTranslator) -set_target_properties(${TARGET} PROPERTIES LINKER_LANGUAGE CXX) - -if (CMAKE_VERSION VERSION_GREATER 3.12) - set_property(TARGET StreamToActionTranslator PROPERTY CXX_STANDARD 23) -endif() - -if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo") -endif () - -target_include_directories(${TARGET} PUBLIC /src ) +project ("StreamToActionTranslator" LANGUAGES CXX) enable_testing() add_subdirectory(tests) +add_subdirectory(src) \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json index f4bc98b..a4a693b 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -8,14 +8,14 @@ "binaryDir": "${sourceDir}/out/build/${presetName}", "installDir": "${sourceDir}/out/install/${presetName}", "cacheVariables": { - "CMAKE_C_COMPILER": "cl.exe", - "CMAKE_CXX_COMPILER": "cl.exe" + "CMAKE_C_COMPILER": "cl", + "CMAKE_CXX_COMPILER": "cl" }, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } }, { "name": "x64-debug", @@ -25,9 +25,9 @@ "value": "x64", "strategy": "external" }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } }, { "name": "x64-release", @@ -37,26 +37,6 @@ "CMAKE_BUILD_TYPE": "Release" } }, - { - "name": "x86-debug", - "displayName": "x86 Debug", - "inherits": "windows-base", - "architecture": { - "value": "x86", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "x86-release", - "displayName": "x86 Release", - "inherits": "x86-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, { "name": "linux-debug", "displayName": "Linux Debug", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..d999c50 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,12 @@ +# CMakeList.txt : CMake project for test driver program, include source and define +cmake_minimum_required (VERSION 3.8) + +set(DRIVER_NAME test_driver) # driver name (change if you wish) +add_executable (${DRIVER_NAME} "driver_main.cpp" "StreamToActionTranslator.h") +target_compile_features(${DRIVER_NAME} PUBLIC cxx_std_23) + +# Enable Hot Reload for MSVC compilers if supported. +if (POLICY CMP0141) + cmake_policy(SET CMP0141 NEW) + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$,$>,$<$:EditAndContinue>,$<$:ProgramDatabase>>") +endif() \ No newline at end of file diff --git a/src/StreamToActionTranslator.h b/src/StreamToActionTranslator.h index 71b058f..44693ca 100644 --- a/src/StreamToActionTranslator.h +++ b/src/StreamToActionTranslator.h @@ -1,6 +1,4 @@ -#pragma once - -/* +/* This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. @@ -11,7 +9,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI For more information, please refer to https://unlicense.org */ - +#pragma once #include #include #include @@ -26,27 +24,35 @@ For more information, please refer to https://unlicense.org #include #include #include - +#include +#include +#include +#include namespace sds { namespace chron = std::chrono; - using Index_t = uint32_t; + using Index_t = uint16_t; using Nanos_t = chron::nanoseconds; using Clock_t = chron::steady_clock; using TimePoint_t = chron::time_point ; using Fn_t = std::function; - using GrpVal_t = int32_t; + using GrpVal_t = int16_t; + template using SmallVector_t = std::vector; + template using SmallFlatMap_t = std::map; enum class RepeatType { - Infinite, // Upon the button being held down, will translate to the key-repeat function activating repeatedly using a delay in between repeats. - FirstOnly, // Upon the button being held down, will send a single repeat, will not continue translating to repeat after the single repeat. - None // No key-repeats sent. + // Upon the button being held down, will translate to the key-repeat function activating repeatedly using a delay in between repeats. + Infinite, + // Upon the button being held down, will send a single repeat, will not continue translating to repeat after the single repeat. + FirstOnly, + // No key-repeats sent. + None }; enum class ActionState @@ -58,6 +64,9 @@ namespace sds }; struct MappingContainer; + struct TranslationPack; + class Translator; + // Concept for range of ButtonDescription type that must be contiguous. template concept MappingRange_c = requires (T & t) @@ -66,19 +75,16 @@ namespace sds { std::ranges::contiguous_range == true }; }; - struct TranslationPack; - class Translator; - // A translator type, wherein you can call GetUpdatedState with a range of virtual keycode integral values, and get a TranslationPack as a result. template concept InputTranslator_c = requires(Poller_t & t) { { t.GetUpdatedState({ 1, 2, 3 }) } -> std::convertible_to; - { t.GetMappingsRange() } -> std::convertible_to>; + { t.GetMappingsRange() } -> std::convertible_to>>; }; template - concept NotBoolIntegral_c = requires(Int_t& t) + concept NotBoolIntegral_c = requires(Int_t & t) { { std::same_as == false }; { std::integral == true }; @@ -96,25 +102,15 @@ namespace sds { t.UpdateForNewMatchingGroupingUp(1) } -> std::convertible_to>; }; - ///** - // * \brief Filter concept, used to apply a specific "overtaking" behavior (exclusivity grouping behavior) implementation. - // */ - //template - //concept ValidFilterType_c = requires(FilterType_t & t) - //{ - // { FilterType_t{ Translator{{}} } }; - // { t.GetFilteredButtonState({ 1, 2, 3 }) } -> std::convertible_to>; - // { std::movable == true }; - //}; - /** * \brief DelayTimer manages a non-blocking time delay, it provides functions such as IsElapsed() and Reset(...) */ - class DelayTimer final + class DelayTimer { TimePoint_t m_start_time{ Clock_t::now() }; Nanos_t m_delayTime{}; // this should remain nanoseconds to ensure maximum granularity when Reset() with a different type. - mutable bool m_has_fired{ false }; + public: + static constexpr Nanos_t DefaultKeyRepeatDelay{ std::chrono::milliseconds{1} }; public: DelayTimer() = delete; explicit DelayTimer(Nanos_t duration) noexcept : m_delayTime(duration) { } @@ -122,19 +118,7 @@ namespace sds DelayTimer(DelayTimer&& other) = default; DelayTimer& operator=(const DelayTimer& other) = default; DelayTimer& operator=(DelayTimer&& other) = default; - ~DelayTimer() = default; - /** - * \brief Operator<< overload for ostream specialization, writes more detailed delay details for debugging. - */ - friend std::ostream& operator<<(std::ostream& os, const DelayTimer& obj) noexcept - { - os << "[DelayTimer]" << '\n' - << "m_start_time:" << obj.m_start_time.time_since_epoch() << '\n' - << "m_delayTime:" << obj.m_delayTime << '\n' - << "m_has_fired:" << obj.m_has_fired << '\n' - << "[/DelayTimer]"; - return os; - } + ~DelayTimer() noexcept = default; /** * \brief Check for elapsed. * \return true if timer has elapsed, false otherwise @@ -144,7 +128,6 @@ namespace sds { if (Clock_t::now() > (m_start_time + m_delayTime)) { - m_has_fired = true; return true; } return false; @@ -156,7 +139,6 @@ namespace sds void Reset(const Nanos_t delay) noexcept { m_start_time = Clock_t::now(); - m_has_fired = false; m_delayTime = { delay }; } /** @@ -165,7 +147,6 @@ namespace sds void Reset() noexcept { m_start_time = Clock_t::now(); - m_has_fired = false; } /** * \brief Gets the current timer period/duration for elapsing. @@ -176,62 +157,15 @@ namespace sds } }; - /** - * \brief Functions called when a state change occurs. - */ - struct KeyStateBehaviors final - { - Fn_t OnDown; // Key-down - Fn_t OnUp; // Key-up - Fn_t OnRepeat; // Key-repeat - Fn_t OnReset; // Reset after key-up and prior to another key-down can be performed - }; - static_assert(std::copyable); - static_assert(std::movable); - - /** - * \brief Controller button to action mapping. This is how a mapping of a controller button to an action is described. - */ - struct ButtonDescription final - { - /** - * \brief Controller button Virtual Keycode. Can be platform dependent or custom mapping, depends on input poller behavior. - */ - int32_t ButtonVirtualKeycode{}; - - /** - * \brief Type of key-repeat behavior. - */ - RepeatType RepeatingKeyBehavior{}; - - /** - * \brief The exclusivity grouping member is intended to allow the user to add different groups of mappings - * that require another mapping from the same group to be "overtaken" or key-up sent before the "overtaking" new mapping - * can perform the key-down. - * \remarks optional, if not in use set to default constructed value or '{}' - */ - std::optional ExclusivityGrouping; // TODO one variation of ex. group behavior is to have a priority value associated with the mapping. - public: - ButtonDescription( - const int32_t buttonCode, - std::optional repeatBehavior = {}, - std::optional optExclusivityGrouping = {}) noexcept - : ButtonVirtualKeycode(buttonCode), - RepeatingKeyBehavior(repeatBehavior.value_or(RepeatType::Infinite)), - ExclusivityGrouping(optExclusivityGrouping) - { - } - }; - static_assert(std::copyable); - static_assert(std::movable); - /** * \brief Wrapper for button to action mapping state enum, the least I can do is make sure state modifications occur through a managing class, * and that there exists only one 'current' state, and that it can only be a finite set of possibilities. * Also contains last sent time (for key-repeat), and delay before first key-repeat timer. * \remarks This class enforces an invariant that it's state cannot be altered out of sequence. */ - class MappingStateTracker final + class + //alignas(std::hardware_constructive_interference_size) + MappingStateTracker { /** * \brief Key Repeat Delay is the time delay a button has in-between activations. @@ -292,53 +226,23 @@ namespace sds static_assert(std::copyable); static_assert(std::movable); - struct MappingContainer final + struct MappingContainer { + Fn_t OnDown; // Key-down + Fn_t OnUp; // Key-up + Fn_t OnRepeat; // Key-repeat + Fn_t OnReset; // Reset after key-up and prior to another key-down can be performed + int32_t ButtonVirtualKeycode{}; + RepeatType RepeatingKeyBehavior{}; /** - * \brief The mapping description. - */ - ButtonDescription Button; - - /** - * \brief The events/functions associated with different states. + * \brief The exclusivity grouping member is intended to allow the user to add different groups of mappings + * that require another mapping from the same group to be "overtaken" or key-up sent before the "overtaking" new mapping + * can perform the key-down. + * \remarks optional, if not in use set to default constructed value or '{}' */ - KeyStateBehaviors StateFunctions; - - /** - * \brief Mutable last action performed, with get/set methods. - */ - MappingStateTracker LastAction; - - public: - MappingContainer( - const ButtonDescription& buttonDescription, - const KeyStateBehaviors& stateFunctions, - std::optional beforeRepeatDelay = {}, - std::optional betweenRepeatDelay = {}) - : - Button(buttonDescription), - StateFunctions(stateFunctions) - { - if (beforeRepeatDelay) - LastAction.DelayBeforeFirstRepeat.Reset(beforeRepeatDelay.value()); - if (betweenRepeatDelay) - LastAction.LastSentTime.Reset(betweenRepeatDelay.value()); - } - - MappingContainer( - ButtonDescription&& buttonDescription, - KeyStateBehaviors&& stateFunctions, - std::optional beforeRepeatDelay = {}, - std::optional betweenRepeatDelay = {}) - : - Button(std::move(buttonDescription)), - StateFunctions(std::move(stateFunctions)) - { - if (beforeRepeatDelay) - LastAction.DelayBeforeFirstRepeat.Reset(beforeRepeatDelay.value()); - if (betweenRepeatDelay) - LastAction.LastSentTime.Reset(betweenRepeatDelay.value()); - } + std::optional ExclusivityGrouping; // TODO one variation of ex. group behavior is to have a priority value associated with the mapping. + std::optional DelayBeforeFirstRepeat; + std::optional BetweenRepeatDelay; }; static_assert(std::copyable); static_assert(std::movable); @@ -351,14 +255,15 @@ namespace sds * design, it would be possible to, for instance, withhold calling the operation to perform, yet still have the mapping's state updating internally, erroneously. * I believe this will make calling order-dependent functionality less error-prone. */ - struct TranslationResult final + struct + //alignas(std::hardware_constructive_interference_size) + TranslationResult { - // TODO test with std::unique_ptr to Fn_t, it currently is like 18k of stack space. // Operation being requested to be performed, callable Fn_t OperationToPerform; // Function to advance the button mapping to the next state (after operation has been performed) Fn_t AdvanceStateFn; - // Hash of the mapping it refers to + // Vk of the mapping it refers to int32_t MappingVk{}; // Exclusivity grouping value, if any std::optional ExclusivityGrouping; @@ -377,7 +282,9 @@ namespace sds * \remarks If using the provided call operator, it will prioritize key-up requests, then key-down requests, then repeat requests, then updates. * I figure it should process key-ups and new key-downs with the highest priority, after that keys doing a repeat, and lastly updates. */ - struct TranslationPack final + struct + //alignas(std::hardware_constructive_interference_size) + TranslationPack { void operator()() const { @@ -399,106 +306,123 @@ namespace sds SmallVector_t UpdateRequests{}; // resets // TODO might wrap the vectors in a struct with a call operator to have individual call operators for range of TranslationResult. }; + //static_assert(sizeof(TranslationPack) > 0); static_assert(std::copyable); static_assert(std::movable); +#pragma region Factory_Functions_For_Translator + // These are a few 'factory' functions, to create the appropriate TranslationResult for the next mapping state--they are tremendously useful. - [[nodiscard]] inline auto GetResetTranslationResult(MappingContainer& currentMapping) noexcept -> TranslationResult + [[nodiscard]] inline auto GetResetTranslationResult(const MappingContainer& currentMapping, MappingStateTracker& stateTracker) noexcept -> TranslationResult { return TranslationResult { - .OperationToPerform = [¤tMapping]() + .OperationToPerform = [&]() { - if (currentMapping.StateFunctions.OnReset) - currentMapping.StateFunctions.OnReset(); + if (currentMapping.OnReset) + currentMapping.OnReset(); }, - .AdvanceStateFn = [¤tMapping]() + .AdvanceStateFn = [&]() { - currentMapping.LastAction.SetInitial(); - currentMapping.LastAction.LastSentTime.Reset(); + stateTracker.SetInitial(); + stateTracker.LastSentTime.Reset(); }, - .MappingVk = currentMapping.Button.ButtonVirtualKeycode, - .ExclusivityGrouping = currentMapping.Button.ExclusivityGrouping + .MappingVk = currentMapping.ButtonVirtualKeycode, + .ExclusivityGrouping = currentMapping.ExclusivityGrouping }; } - [[nodiscard]] inline auto GetRepeatTranslationResult(MappingContainer& currentMapping) noexcept -> TranslationResult + [[nodiscard]] inline auto GetRepeatTranslationResult(const MappingContainer& currentMapping, MappingStateTracker& stateTracker) noexcept -> TranslationResult { return TranslationResult { - .OperationToPerform = [¤tMapping]() + .OperationToPerform = [&]() { - if (currentMapping.StateFunctions.OnRepeat) - currentMapping.StateFunctions.OnRepeat(); - currentMapping.LastAction.LastSentTime.Reset(); + if (currentMapping.OnRepeat) + currentMapping.OnRepeat(); + stateTracker.LastSentTime.Reset(); }, - .AdvanceStateFn = [¤tMapping]() + .AdvanceStateFn = [&]() { - currentMapping.LastAction.SetRepeat(); + stateTracker.SetRepeat(); }, - .MappingVk = currentMapping.Button.ButtonVirtualKeycode, - .ExclusivityGrouping = currentMapping.Button.ExclusivityGrouping + .MappingVk = currentMapping.ButtonVirtualKeycode, + .ExclusivityGrouping = currentMapping.ExclusivityGrouping }; } - [[nodiscard]] inline auto GetOvertakenTranslationResult(MappingContainer& overtakenMapping) noexcept -> TranslationResult + [[nodiscard]] inline auto GetOvertakenTranslationResult(const MappingContainer& overtakenMapping, MappingStateTracker& stateTracker) noexcept -> TranslationResult { return TranslationResult { - .OperationToPerform = [&overtakenMapping]() + .OperationToPerform = [&]() { - if (overtakenMapping.StateFunctions.OnUp) - overtakenMapping.StateFunctions.OnUp(); + if (overtakenMapping.OnUp) + overtakenMapping.OnUp(); }, - .AdvanceStateFn = [&overtakenMapping]() + .AdvanceStateFn = [&]() { - overtakenMapping.LastAction.SetUp(); + stateTracker.SetUp(); }, - .MappingVk = overtakenMapping.Button.ButtonVirtualKeycode, - .ExclusivityGrouping = overtakenMapping.Button.ExclusivityGrouping + .MappingVk = overtakenMapping.ButtonVirtualKeycode, + .ExclusivityGrouping = overtakenMapping.ExclusivityGrouping }; } - [[nodiscard]] inline auto GetKeyUpTranslationResult(MappingContainer& currentMapping) noexcept -> TranslationResult + [[nodiscard]] inline auto GetKeyUpTranslationResult(const MappingContainer& currentMapping, MappingStateTracker& stateTracker) noexcept -> TranslationResult { return TranslationResult { - .OperationToPerform = [¤tMapping]() + .OperationToPerform = [&]() { - if (currentMapping.StateFunctions.OnUp) - currentMapping.StateFunctions.OnUp(); + if (currentMapping.OnUp) + currentMapping.OnUp(); }, - .AdvanceStateFn = [¤tMapping]() + .AdvanceStateFn = [&]() { - currentMapping.LastAction.SetUp(); + stateTracker.SetUp(); }, - .MappingVk = currentMapping.Button.ButtonVirtualKeycode, - .ExclusivityGrouping = currentMapping.Button.ExclusivityGrouping + .MappingVk = currentMapping.ButtonVirtualKeycode, + .ExclusivityGrouping = currentMapping.ExclusivityGrouping }; } - [[nodiscard]] inline auto GetInitialKeyDownTranslationResult(MappingContainer& currentMapping) noexcept -> TranslationResult + [[nodiscard]] inline auto GetInitialKeyDownTranslationResult(const MappingContainer& currentMapping, MappingStateTracker& stateTracker) noexcept -> TranslationResult { return TranslationResult { - .OperationToPerform = [¤tMapping]() + .OperationToPerform = [&]() { - if (currentMapping.StateFunctions.OnDown) - currentMapping.StateFunctions.OnDown(); + if (currentMapping.OnDown) + currentMapping.OnDown(); // Reset timer after activation, to wait for elapsed before another next state translation is returned. - currentMapping.LastAction.LastSentTime.Reset(); - currentMapping.LastAction.DelayBeforeFirstRepeat.Reset(); + stateTracker.LastSentTime.Reset(); + stateTracker.DelayBeforeFirstRepeat.Reset(); }, - .AdvanceStateFn = [¤tMapping]() + .AdvanceStateFn = [&]() { - currentMapping.LastAction.SetDown(); + stateTracker.SetDown(); }, - .MappingVk = currentMapping.Button.ButtonVirtualKeycode, - .ExclusivityGrouping = currentMapping.Button.ExclusivityGrouping + .MappingVk = currentMapping.ButtonVirtualKeycode, + .ExclusivityGrouping = currentMapping.ExclusivityGrouping }; } +#pragma endregion + +#pragma region Algos_For_Translator // Algorithm functions used by the translator. + + [[nodiscard]] constexpr bool IsNotEnd(const std::ranges::range auto& theRange, const std::ranges::iterator_t& theIterator) noexcept + { + return theIterator != std::ranges::cend(theRange); + } + + [[nodiscard]] constexpr bool IsEnd(const std::ranges::range auto& theRange, const std::ranges::iterator_t& theIterator) noexcept + { + return theIterator == std::ranges::cend(theRange); + } + /** * \brief For a single mapping, search the controller state update buffer and produce a TranslationResult appropriate to the current mapping state and controller state. * \param downKeys Wrapper class containing the results of a controller state update polling. @@ -506,128 +430,103 @@ namespace sds * \returns Optional, TranslationResult */ template - [[nodiscard]] auto GetButtonTranslationForInitialToDown(const SmallVector_t& downKeys, MappingContainer& singleButton) noexcept -> std::optional + [[nodiscard]] auto GetButtonTranslationForInitialToDown(const SmallVector_t& downKeys, const MappingContainer& singleButton, MappingStateTracker& stateTracker) noexcept -> std::optional { - using std::ranges::find, std::ranges::end; + using std::ranges::find; - if (singleButton.LastAction.IsInitialState()) + if (stateTracker.IsInitialState()) { - const auto findResult = find(downKeys, singleButton.Button.ButtonVirtualKeycode); + const auto findResult = find(downKeys, singleButton.ButtonVirtualKeycode); // If VK *is* found in the down list, create the down translation. - if (findResult != end(downKeys)) - return GetInitialKeyDownTranslationResult(singleButton); + if (IsNotEnd(downKeys, findResult)) + return std::make_optional(GetInitialKeyDownTranslationResult(singleButton, stateTracker)); } return {}; } template - [[nodiscard]] auto GetButtonTranslationForDownToRepeat(const SmallVector_t& downKeys, MappingContainer& singleButton) noexcept -> std::optional + [[nodiscard]] auto GetButtonTranslationForDownToRepeat(const SmallVector_t& downKeys, const MappingContainer& singleButton, MappingStateTracker& stateTracker) noexcept -> std::optional { - using std::ranges::find, std::ranges::end; - const bool isDownAndUsesRepeat = singleButton.LastAction.IsDown() && - (singleButton.Button.RepeatingKeyBehavior == RepeatType::Infinite || singleButton.Button.RepeatingKeyBehavior == RepeatType::FirstOnly); - const bool isDelayElapsed = singleButton.LastAction.DelayBeforeFirstRepeat.IsElapsed(); + using std::ranges::find; + + const bool isDownAndUsesRepeat = + stateTracker.IsDown() + && (singleButton.RepeatingKeyBehavior == RepeatType::Infinite + || singleButton.RepeatingKeyBehavior == RepeatType::FirstOnly); + + const bool isDelayElapsed = stateTracker.DelayBeforeFirstRepeat.IsElapsed(); + if (isDownAndUsesRepeat && isDelayElapsed) { - const auto findResult = find(downKeys, singleButton.Button.ButtonVirtualKeycode); + const auto findResult = find(downKeys, singleButton.ButtonVirtualKeycode); // If VK *is* found in the down list, create the repeat translation. - if (findResult != end(downKeys)) - return GetRepeatTranslationResult(singleButton); + if (IsNotEnd(downKeys, findResult)) + return std::make_optional(GetRepeatTranslationResult(singleButton, stateTracker)); } return {}; } template - [[nodiscard]] auto GetButtonTranslationForRepeatToRepeat(const SmallVector_t& downKeys, MappingContainer& singleButton) noexcept -> std::optional + [[nodiscard]] auto GetButtonTranslationForRepeatToRepeat(const SmallVector_t& downKeys, const MappingContainer& singleButton, MappingStateTracker& stateTracker) noexcept -> std::optional { - using std::ranges::find, std::ranges::end; - const bool isRepeatAndUsesInfinite = singleButton.LastAction.IsRepeating() && singleButton.Button.RepeatingKeyBehavior == RepeatType::Infinite; - if (isRepeatAndUsesInfinite && singleButton.LastAction.LastSentTime.IsElapsed()) + using std::ranges::find; + + const bool isRepeatAndUsesInfinite = stateTracker.IsRepeating() && singleButton.RepeatingKeyBehavior == RepeatType::Infinite; + if (isRepeatAndUsesInfinite && stateTracker.LastSentTime.IsElapsed()) { - const auto findResult = find(downKeys, singleButton.Button.ButtonVirtualKeycode); + const auto findResult = find(downKeys, singleButton.ButtonVirtualKeycode); // If VK *is* found in the down list, create the repeat translation. - if (findResult != end(downKeys)) - return GetRepeatTranslationResult(singleButton); + if (IsNotEnd(downKeys, findResult)) + return std::make_optional(GetRepeatTranslationResult(singleButton, stateTracker)); } return {}; } template - [[nodiscard]] auto GetButtonTranslationForDownOrRepeatToUp(const SmallVector_t& downKeys, MappingContainer& singleButton) noexcept -> std::optional + [[nodiscard]] auto GetButtonTranslationForDownOrRepeatToUp(const SmallVector_t& downKeys, const MappingContainer& singleButton, MappingStateTracker& stateTracker) noexcept -> std::optional { - using std::ranges::find, std::ranges::end; - if (singleButton.LastAction.IsDown() || singleButton.LastAction.IsRepeating()) + using std::ranges::find; + + if (stateTracker.IsDown() || stateTracker.IsRepeating()) { - const auto findResult = find(downKeys, singleButton.Button.ButtonVirtualKeycode); + const auto findResult = find(downKeys, singleButton.ButtonVirtualKeycode); // If VK is not found in the down list, create the up translation. - if (findResult == end(downKeys)) - return GetKeyUpTranslationResult(singleButton); + if (IsEnd(downKeys, findResult)) + return std::make_optional(GetKeyUpTranslationResult(singleButton, stateTracker)); } return {}; } // This is the reset translation - [[nodiscard]] inline auto GetButtonTranslationForUpToInitial(MappingContainer& singleButton) noexcept -> std::optional + [[nodiscard]] inline auto GetButtonTranslationForUpToInitial(const MappingContainer& singleButton, MappingStateTracker& stateTracker) noexcept -> std::optional { // if the timer has elapsed, update back to the initial state. - if (singleButton.LastAction.IsUp() && singleButton.LastAction.LastSentTime.IsElapsed()) + if (stateTracker.IsUp() && stateTracker.LastSentTime.IsElapsed()) { - return GetResetTranslationResult(singleButton); + return std::make_optional(GetResetTranslationResult(singleButton, stateTracker)); } return {}; } /** - * \brief Returns the indices at which a mapping that matches the 'vk' was found. PRECONDITION: A mapping with the specified VK does exist in the mappingsRange! + * \brief Optionally returns the indices at which a mapping that matches the 'vk' was found. * \param vk Virtual keycode of the presumably 'down' key with which to match MappingContainer mappings. - * \param mappingsRange The range of MappingContainer mappings for which to return the indices of matching mappings. + * \param mappingsRange The range of MappingContainer mappings for which to return the index of a matching mapping. */ - [[nodiscard]] auto GetMappingIndexForVk(const NotBoolIntegral_c auto vk, const std::span mappingsRange) -> std::optional + [[nodiscard]] inline auto GetMappingIndexForVk(const NotBoolIntegral_c auto vk, const std::span mappingsRange) noexcept -> std::optional { using std::ranges::find_if; - using std::ranges::cend; - using std::ranges::cbegin; - using std::ranges::distance; - const auto findResult = find_if(mappingsRange, [vk](const auto e) { return e.Button.ButtonVirtualKeycode == vk; }); - const bool didFindResult = findResult != cend(mappingsRange); - - if (!didFindResult) - { - throw std::runtime_error( - std::vformat("Did not find mapping with vk: {} in mappings range.\nLocation:\n{}\n\n", - std::make_format_args(static_cast(vk), std::source_location::current().function_name()))); - } - - return static_cast(distance(cbegin(mappingsRange), findResult)); - } - - /** - * \brief Returns the iterator at which a mapping that matches the 'vk' was found. - * \param vk Virtual keycode of the presumably 'down' key with which to match MappingContainer mappings. - * \param mappingsRange The range of MappingContainer mappings for which to return the indices of matching mappings. - */ - [[nodiscard]] auto GetMappingByVk(const NotBoolIntegral_c auto vk, std::span mappingsRange) -> std::optional::iterator> - { - using std::ranges::find_if; - using std::ranges::cend; - using std::ranges::cbegin; - using std::ranges::distance; - - const auto findResult = find_if(mappingsRange, [vk](const auto e) { return e.Button.ButtonVirtualKeycode == vk; }); - const bool didFindResult = findResult != cend(mappingsRange); + const auto findResult = find_if(mappingsRange, [vk](const auto& e) { return e.ButtonVirtualKeycode == static_cast(vk); }); + const bool didFindResult = IsNotEnd(mappingsRange, findResult); + [[unlikely]] if (!didFindResult) { return {}; } - return findResult; - } - - [[nodiscard]] constexpr auto IsVkInStateUpdate(const NotBoolIntegral_c auto vkToFind, const std::span downVirtualKeys) noexcept -> bool - { - return std::ranges::any_of(downVirtualKeys, [vkToFind](const auto vk) { return vk == vkToFind; }); + return std::make_optional(static_cast(std::distance(mappingsRange.cbegin(), findResult))); } [[nodiscard]] constexpr auto IsMappingInRange(const NotBoolIntegral_c auto vkToFind, const std::ranges::range auto& downVirtualKeys) noexcept -> bool @@ -635,14 +534,10 @@ namespace sds return std::ranges::any_of(downVirtualKeys, [vkToFind](const auto vk) { return vk == vkToFind; }); } - constexpr void EraseValuesFromRange(std::ranges::range auto& theRange, const std::ranges::range auto& theValues) noexcept + constexpr auto GetErasedRange(const std::ranges::range auto& theRange, const std::ranges::range auto& theValues) noexcept -> std::vector { - for (const auto& elem : theValues) - { - const auto foundPosition = std::ranges::find(theRange, elem); - if (foundPosition != std::ranges::cend(theRange)) - theRange.erase(foundPosition); - } + auto copied = theRange | std::views::filter([&](const auto& elem) { return !IsMappingInRange(elem, theValues); }); + return { std::ranges::begin(copied), std::ranges::end(copied) }; } /** @@ -655,11 +550,11 @@ namespace sds SmallFlatMap_t mappingTable; for (const auto& e : mappingsList) { - if (mappingTable[e.Button.ButtonVirtualKeycode]) + if (mappingTable[e.ButtonVirtualKeycode]) { return false; } - mappingTable[e.Button.ButtonVirtualKeycode] = true; + mappingTable[e.ButtonVirtualKeycode] = true; } return true; } @@ -671,7 +566,7 @@ namespace sds */ [[nodiscard]] inline bool AreMappingVksNonZero(const std::span mappingsList) noexcept { - return !std::ranges::any_of(mappingsList, [](const auto vk) { return vk.ButtonVirtualKeycode == 0; }, &MappingContainer::Button); + return !std::ranges::any_of(mappingsList, [](const auto vk) { return vk == 0; }, &MappingContainer::ButtonVirtualKeycode); } /** @@ -684,6 +579,8 @@ namespace sds return mapping.IsDown() || mapping.IsRepeating(); } +#pragma endregion Algos_For_Translator + /** * \brief Encapsulates the mapping buffer, processes controller state updates, returns translation packs. * \remarks If, before destruction, the mappings are in a state other than initial or awaiting reset, then you may wish to @@ -691,13 +588,13 @@ namespace sds *

*

An invariant exists such that: There must be only one mapping per virtual keycode.

*/ - class Translator final + class Translator { using MappingVector_t = SmallVector_t; + using MappingStateVector_t = SmallVector_t; static_assert(MappingRange_c); - - MappingVector_t m_mappings; - + MappingStateVector_t m_mappingStates; + std::shared_ptr m_mappings; public: Translator() = delete; // no default Translator(const Translator& other) = delete; // no copy @@ -712,41 +609,52 @@ namespace sds * \param keyMappings mapping vector type * \exception std::runtime_error on exclusivity group error during construction, OR more than one mapping per VK. */ - explicit Translator(MappingVector_t&& keyMappings) - : m_mappings(std::move(keyMappings)) + explicit Translator(MappingRange_c auto&& keyMappings) + : m_mappings(std::make_shared(std::forward(keyMappings))) { - if (!AreMappingsUniquePerVk(m_mappings) || !AreMappingVksNonZero(m_mappings)) + if (!AreMappingsUniquePerVk(*m_mappings) || !AreMappingVksNonZero(*m_mappings)) throw std::runtime_error("Exception: More than 1 mapping per VK!"); + + m_mappingStates.resize(m_mappings->size()); + // Zip returns a tuple of refs to the types. + for (auto zipped : std::views::zip(*m_mappings, m_mappingStates)) + { + const auto& mapping = std::get<0>(zipped); + auto& mappingState = std::get<1>(zipped); + mappingState.DelayBeforeFirstRepeat.Reset(mapping.DelayBeforeFirstRepeat.value_or(DelayTimer::DefaultKeyRepeatDelay)); + mappingState.LastSentTime.Reset(mapping.BetweenRepeatDelay.value_or(DelayTimer::DefaultKeyRepeatDelay)); + } } public: - [[nodiscard]] auto operator()(SmallVector_t stateUpdate) noexcept -> TranslationPack + [[nodiscard]] auto operator()(const SmallVector_t& stateUpdate) noexcept -> TranslationPack { - return GetUpdatedState(std::move(stateUpdate)); + return GetUpdatedState(stateUpdate); } - [[nodiscard]] auto GetUpdatedState(SmallVector_t&& stateUpdate) noexcept -> TranslationPack + [[nodiscard]] auto GetUpdatedState(const SmallVector_t& stateUpdate) noexcept -> TranslationPack { TranslationPack translations; - for (auto& mapping : m_mappings) + for (auto elem : std::views::zip(*m_mappings, m_mappingStates)) { - if (const auto upToInitial = GetButtonTranslationForUpToInitial(mapping)) + auto& [mapping, mappingState] = elem; + if (const auto upToInitial = GetButtonTranslationForUpToInitial(mapping, mappingState)) { translations.UpdateRequests.push_back(*upToInitial); } - else if (const auto initialToDown = GetButtonTranslationForInitialToDown(stateUpdate, mapping)) + else if (const auto initialToDown = GetButtonTranslationForInitialToDown(stateUpdate, mapping, mappingState)) { // Advance to next state. translations.DownRequests.push_back(*initialToDown); } - else if (const auto downToFirstRepeat = GetButtonTranslationForDownToRepeat(stateUpdate, mapping)) + else if (const auto downToFirstRepeat = GetButtonTranslationForDownToRepeat(stateUpdate, mapping, mappingState)) { translations.RepeatRequests.push_back(*downToFirstRepeat); } - else if (const auto repeatToRepeat = GetButtonTranslationForRepeatToRepeat(stateUpdate, mapping)) + else if (const auto repeatToRepeat = GetButtonTranslationForRepeatToRepeat(stateUpdate, mapping, mappingState)) { translations.RepeatRequests.push_back(*repeatToRepeat); } - else if (const auto repeatToUp = GetButtonTranslationForDownOrRepeatToUp(stateUpdate, mapping)) + else if (const auto repeatToUp = GetButtonTranslationForDownOrRepeatToUp(stateUpdate, mapping, mappingState)) { translations.UpRequests.push_back(*repeatToUp); } @@ -757,17 +665,18 @@ namespace sds [[nodiscard]] auto GetCleanupActions() noexcept -> SmallVector_t { SmallVector_t translations; - for (auto& mapping : m_mappings) + for (auto elem : std::views::zip(*m_mappings, m_mappingStates)) { - if (DoesMappingNeedCleanup(mapping.LastAction)) + auto& [mapping, mappingState] = elem; + if (DoesMappingNeedCleanup(mappingState)) { - translations.push_back(GetKeyUpTranslationResult(mapping)); + translations.push_back(GetKeyUpTranslationResult(mapping, mappingState)); } } return translations; } - [[nodiscard]] auto GetMappingsRange() const -> const MappingVector_t& + [[nodiscard]] auto GetMappingsRange() const noexcept -> std::shared_ptr { return m_mappings; } @@ -783,7 +692,7 @@ namespace sds * \remarks This abstraction manages the currently activated key being "overtaken" by another key from the same group and causing a key-up/down to be sent for the currently activated, * as well as moving the key in line behind the newly activated key. A much needed abstraction. */ - class GroupActivationInfo final + class GroupActivationInfo { using Elem_t = int32_t; @@ -809,7 +718,7 @@ namespace sds { const auto currentDownValue = ActivatedValuesQueue.front(); ActivatedValuesQueue.push_front(newDownVk); - return std::make_pair(false, currentDownValue); + return std::make_pair(false, std::make_optional(currentDownValue)); } // New activated mapping case, add to queue in first position and don't filter. No key-up required. @@ -838,12 +747,12 @@ namespace sds // Case wherein the currently activated mapping is the one getting a key-up. if (isInFirstPosition) { - if (ActivatedValuesQueue.size() > 1) + if (!ActivatedValuesQueue.empty()) { // If there is an overtaken queue, key-down the next key in line. ActivatedValuesQueue.pop_front(); // Return the new front hash to be sent a key-down. - return ActivatedValuesQueue.front(); + return !ActivatedValuesQueue.empty() ? std::make_optional(ActivatedValuesQueue.front()) : std::optional{}; } } @@ -871,7 +780,8 @@ namespace sds const bool isFound = findResult != std::ranges::cend(ActivatedValuesQueue); return !isCurrentActivation && isFound; } - [[nodiscard]] bool IsAnyMappingActivated() const noexcept { + [[nodiscard]] bool IsAnyMappingActivated() const noexcept + { return !ActivatedValuesQueue.empty(); } [[nodiscard]] bool IsMappingActivatedOrOvertaken(const Elem_t vk) const noexcept @@ -895,38 +805,47 @@ namespace sds * it will only process a single overtaking key-down at a time, and will suppress the rest in the state update to be handled on the next iteration. */ template - class OvertakingFilter final + class OvertakingFilter { - using VirtualCode_t = int32_t; - - // Mapping of exclusivity grouping value to - using MapType_t = std::map; + using VirtualCode_t = int32_t; - // span to mappings - std::span m_mappings; + std::unordered_set m_allVirtualKeycodes; // Order does not match mappings + // const ptr to mappings + std::shared_ptr> m_mappings; // map of grouping value to GroupActivationInfo container. - MapType_t m_groupMap; + std::unordered_map m_groupMap; + // Mapping of grouping value to mapping indices. + std::unordered_map> m_groupToVkMap; + + std::unordered_map m_vkToIndexMap; public: - OvertakingFilter() = delete; + OvertakingFilter() noexcept = delete; - explicit OvertakingFilter(const InputTranslator_c auto& translator) + explicit OvertakingFilter(const InputTranslator_c auto& translator) noexcept { - SetMappingRange(translator.GetMappingsRange()); + auto pMappings = translator.GetMappingsRange(); + + SetMappingRange(pMappings); } // This function is used to filter the controller state updates before they are sent to the translator. // It will have an effect on overtaking behavior by modifying the state update buffer, which just contains the virtual keycodes that are reported as down. - [[nodiscard]] auto GetFilteredButtonState(SmallVector_t&& stateUpdate) -> SmallVector_t + [[nodiscard]] auto GetFilteredButtonState(const SmallVector_t& stateUpdate) noexcept -> SmallVector_t { using std::ranges::sort; + using std::views::filter; // Sorting provides an ordering to which down states with an already handled exclusivity grouping get filtered out for this iteration. //sort(stateUpdate, std::ranges::less{}); // TODO <-- problem for the (current) unit testing, optional anyway - stateUpdate = FilterStateUpdateForUniqueExclusivityGroups(std::move(stateUpdate)); + // Filters out VKs that don't have any corresponding mapping. + auto filteredUpdateView = stateUpdate | filter([this](const auto vk) { return m_allVirtualKeycodes.contains(vk); }); + const std::vector filteredStateUpdate = { filteredUpdateView.cbegin(), filteredUpdateView.cend() }; + + const auto uniqueGrouped = GetNonUniqueGroupElements(filteredStateUpdate); - auto filteredForDown = FilterDownTranslation(stateUpdate); + const auto filteredForDown = FilterDownTranslation(uniqueGrouped); // There appears to be no reason to report additional VKs that will become 'down' after a key is moved to up, // because for the key to still be in the overtaken queue, it would need to still be 'down' as well, and thus handled @@ -936,66 +855,80 @@ namespace sds return filteredForDown; } - auto operator()(SmallVector_t stateUpdate) -> SmallVector_t + auto operator()(const SmallVector_t& stateUpdate) noexcept -> SmallVector_t { - return GetFilteredButtonState(std::move(stateUpdate)); + return GetFilteredButtonState(stateUpdate); } private: - void SetMappingRange(const std::span mappingsList) + void SetMappingRange(const std::shared_ptr>& mappingsList) noexcept { m_mappings = mappingsList; + m_allVirtualKeycodes = {}; m_groupMap = {}; + m_groupToVkMap = {}; + m_vkToIndexMap = {}; - // Build the map of ex. group information. - for (const auto& elem : mappingsList) + BuildAllMemos(m_mappings); + } + + // A somewhat important bit of memoization/pre-processing. + void BuildAllMemos(const std::shared_ptr>& mappingsList) noexcept + { + using std::views::enumerate; + + for (const auto& [index, elem] : enumerate(*mappingsList)) { - if (elem.Button.ExclusivityGrouping) + // all vk set + m_allVirtualKeycodes.insert(elem.ButtonVirtualKeycode); + m_vkToIndexMap[elem.ButtonVirtualKeycode] = static_cast(index); + + if (elem.ExclusivityGrouping) { - const auto grpVal = *elem.Button.ExclusivityGrouping; - m_groupMap[grpVal] = {}; + // group to group info map + m_groupMap[elem.ExclusivityGrouping.value()] = {}; + // group to vk map + m_groupToVkMap[elem.ExclusivityGrouping.value()].insert(elem.ButtonVirtualKeycode); } } } - [[nodiscard]] auto FilterDownTranslation(const SmallVector_t& stateUpdate) -> SmallVector_t + [[nodiscard]] auto FilterDownTranslation(const SmallVector_t& stateUpdate) noexcept -> SmallVector_t { - using std::ranges::find; - using std::ranges::cend; - using std::ranges::views::transform; - using std::ranges::views::filter; - using std::erase; - - auto stateUpdateCopy = stateUpdate; + using std::views::filter; + using std::views::transform; - // filters for all mappings of interest per the current 'down' VK buffer. - const auto vkHasMappingPred = [this](const auto vk) -> bool + const auto vkToMappingIndex = [&](const auto vk) -> std::optional { - const auto findResult = GetMappingByVk(vk, m_mappings); - return findResult.has_value(); + using std::ranges::find_if; + if (m_allVirtualKeycodes.contains(vk)) + { + return std::make_optional(m_vkToIndexMap[vk]); + } + return {}; }; - const auto exGroupPred = [this](const auto ind) -> bool + const auto optWithValueAndGroup = [&](const auto opt) -> bool { - return GetMappingAt(ind).Button.ExclusivityGrouping.has_value(); + return opt.has_value() && GetMappingAt(*opt).ExclusivityGrouping.has_value(); }; - const auto mappingIndexPred = [this](const auto vk) -> std::size_t + const auto removeOpt = [&](const auto opt) { - auto findResult = GetMappingIndexForVk(vk, m_mappings); - assert(findResult.has_value()); - return *findResult; + return opt.value(); }; + auto stateUpdateCopy = stateUpdate; SmallVector_t vksToRemoveRange; - - for (const auto index : stateUpdateCopy | filter(vkHasMappingPred) | transform(mappingIndexPred) | filter(exGroupPred)) + // This appeared (at this time) to be the best option: + // input vk List -> xform to mapping index list -> filter results to only include non-empty optional and ex. group -> xform to remove the optional = index list of mappings in the state update that have an ex. group. + for (const auto& mappingIndex : stateUpdateCopy | transform(vkToMappingIndex) | filter(optWithValueAndGroup) | transform(removeOpt)) { - const auto& currentMapping = GetMappingAt(index); - auto& currentGroup = m_groupMap[*currentMapping.Button.ExclusivityGrouping]; + auto& currentMapping = GetMappingAt(mappingIndex); + auto& currentGroup = m_groupMap[*currentMapping.ExclusivityGrouping]; - const auto& [shouldFilter, upOpt] = currentGroup.UpdateForNewMatchingGroupingDown(currentMapping.Button.ButtonVirtualKeycode); + const auto& [shouldFilter, upOpt] = currentGroup.UpdateForNewMatchingGroupingDown(currentMapping.ButtonVirtualKeycode); if (shouldFilter) { - vksToRemoveRange.push_back(currentMapping.Button.ButtonVirtualKeycode); + vksToRemoveRange.push_back(currentMapping.ButtonVirtualKeycode); } if (upOpt) { @@ -1003,49 +936,45 @@ namespace sds } } - EraseValuesFromRange(stateUpdateCopy, vksToRemoveRange); - - return stateUpdateCopy; + return GetErasedRange(stateUpdateCopy, vksToRemoveRange); } // it will process only one key per ex. group per iteration. The others will be filtered out and handled on the next iteration. - void FilterUpTranslation(const SmallVector_t& stateUpdate) + void FilterUpTranslation(const SmallVector_t& stateUpdate) noexcept { - using std::ranges::views::filter; - + using std::views::filter; // filters for all mappings of interest per the current 'down' VK buffer (the UP mappings in this case). - const auto exGroupPred = [](const auto& currentMapping) + const auto exGroupAndNotInUpdatePred = [&](const auto& currentMapping) { - return currentMapping.Button.ExclusivityGrouping.has_value(); - }; - const auto stateUpdateUpPred = [&stateUpdate](const auto& currentMapping) - { - return !IsMappingInRange(currentMapping.Button.ButtonVirtualKeycode, stateUpdate); + const bool hasValue = currentMapping.ExclusivityGrouping.has_value(); + const bool notInUpdate = !IsMappingInRange(currentMapping.ButtonVirtualKeycode, stateUpdate); + return hasValue && notInUpdate; }; - for (const auto& currentMapping : m_mappings | filter(exGroupPred) | filter(stateUpdateUpPred)) + for (const auto& currentMapping : (*m_mappings) | filter(exGroupAndNotInUpdatePred)) { - auto& currentGroup = m_groupMap[*currentMapping.Button.ExclusivityGrouping]; - currentGroup.UpdateForNewMatchingGroupingUp(currentMapping.Button.ButtonVirtualKeycode); + auto& currentGroup = m_groupMap[*currentMapping.ExclusivityGrouping]; + currentGroup.UpdateForNewMatchingGroupingUp(currentMapping.ButtonVirtualKeycode); } } private: - [[nodiscard]] constexpr auto GetMappingAt(const std::size_t index) noexcept -> const MappingContainer& + [[nodiscard]] constexpr auto GetMappingAt(const NotBoolIntegral_c auto index) noexcept -> const MappingContainer& { - return m_mappings[index]; + return m_mappings->at(static_cast(index)); } - /** - * \brief Used to remove VKs with an exclusivity grouping that another state update VK already has. Processed from begin to end, so the first processed VK will be the left-most and - * duplicates to the right will be removed. - * \remarks This is essential because processing more than one exclusivity grouping having mapping in a single iteration of a filter will mean the first ex. group vks were not actually processed - * by the translator, yet their state would be updated by the filter incorrectly. Also, VKs in the state update must be unique! One VK per mapping is a hard precondition. - * \return "filtered" state update. - */ - [[nodiscard]] auto FilterStateUpdateForUniqueExclusivityGroups(SmallVector_t&& stateUpdate) -> SmallVector_t + [[nodiscard]] constexpr auto GetMappingForVk(const NotBoolIntegral_c auto vk) noexcept -> const MappingContainer& { - using std::ranges::find_if, std::ranges::cend, std::ranges::find; + auto ind = GetMappingIndexForVk(vk, *m_mappings); + assert(ind.has_value()); + return GetMappingAt(*ind); + } + + // Pre: VKs in state update do have a mapping. + [[nodiscard]] auto GetNonUniqueGroupElements(const SmallVector_t& stateUpdate) noexcept -> SmallVector_t + { + using std::ranges::find, std::ranges::cend; using StateRange_t = std::remove_cvref_t; SmallVector_t groupingValueBuffer; @@ -1055,38 +984,30 @@ namespace sds for (const auto vk : stateUpdate) { - const auto foundMappingForVk = find_if(m_mappings, [vk](const auto& e) - { - return e.Button.ButtonVirtualKeycode == vk; - }); - if (foundMappingForVk != cend(m_mappings)) + const auto mappingIndex = m_vkToIndexMap[vk]; + const auto& foundMappingForVk = GetMappingAt(mappingIndex); + + if (foundMappingForVk.ExclusivityGrouping) { - if (foundMappingForVk->Button.ExclusivityGrouping) + const auto grpVal = foundMappingForVk.ExclusivityGrouping.value(); + auto& currentGroup = m_groupMap[grpVal]; + if (!currentGroup.IsMappingActivatedOrOvertaken(vk)) { - const auto grpVal = foundMappingForVk->Button.ExclusivityGrouping.value(); - auto& currentGroup = m_groupMap[grpVal]; - if (!currentGroup.IsMappingActivatedOrOvertaken(vk)) - { - const auto groupingFindResult = find(groupingValueBuffer, grpVal); - - // If already in located, being handled groupings, add to remove buffer. - if (groupingFindResult != cend(groupingValueBuffer)) - virtualKeycodesToRemove.push_back(vk); - // Otherwise, add this new grouping to the grouping value buffer. - else - groupingValueBuffer.push_back(grpVal); - } + const auto groupingFindResult = find(groupingValueBuffer, grpVal); + + // If already in located, being handled groupings, add to remove buffer. + if (groupingFindResult != cend(groupingValueBuffer)) + virtualKeycodesToRemove.emplace_back(vk); + // Otherwise, add this new grouping to the grouping value buffer. + else + groupingValueBuffer.emplace_back(grpVal); } } } - - EraseValuesFromRange(stateUpdate, virtualKeycodesToRemove); - - return stateUpdate; + + return GetErasedRange(stateUpdate, virtualKeycodesToRemove); } }; static_assert(std::copyable>); static_assert(std::movable>); - //static_assert(ValidFilterType_c> == true); - } diff --git a/src/driver_main.cpp b/src/driver_main.cpp new file mode 100644 index 0000000..a12ef0d --- /dev/null +++ b/src/driver_main.cpp @@ -0,0 +1,617 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "StreamToActionTranslator.h" +#define NOMINMAX +#include +#include +#pragma comment(lib, "Xinput.lib") + +enum class ThumbstickDirection +{ + Up, + UpRight, + Right, + RightDown, + Down, + DownLeft, + Left, + LeftUp, + Invalid +}; + +enum class ControllerStick +{ + LeftStick, + RightStick +}; + +/** + * \brief Some constants that are not configurable. + */ +struct KeyboardSettings +{ + /** + * \brief Delay each iteration of a polling loop, short enough to not miss information, long enough to not waste CPU cycles. + */ + static constexpr sds::Nanos_t PollingLoopDelay{ std::chrono::milliseconds{1} }; + /** + * \brief Key Repeat Delay is the time delay a button has in-between activations. + */ + static constexpr sds::Nanos_t KeyRepeatDelay{ std::chrono::microseconds{100'000} }; + + // Controller buttons + static constexpr int32_t ButtonA{ 4'096 }; + static constexpr int32_t ButtonB{ 8'192 }; + static constexpr int32_t ButtonX{ 16'384 }; + static constexpr int32_t ButtonY{ 32'768 }; + + // Dpad buttons + static constexpr int32_t DpadUp{ 1 }; + static constexpr int32_t DpadDown{ 2 }; + static constexpr int32_t DpadLeft{ 4 }; + static constexpr int32_t DpadRight{ 8 }; + + // Left thumbstick directions + static constexpr int32_t LeftThumbstickUp{ 211 }; // UP + static constexpr int32_t LeftThumbstickDown{ 212 }; // DOWN + static constexpr int32_t LeftThumbstickRight{ 213 }; // RIGHT + static constexpr int32_t LeftThumbstickLeft{ 214 }; // LEFT + + static constexpr int32_t LeftThumbstickUpLeft{ 22'564 }; // UP-LEFT + static constexpr int32_t LeftThumbstickUpRight{ 22'565 }; // UP-RIGHT + static constexpr int32_t LeftThumbstickDownRight{ 22'566 }; // RIGHT-DOWN + static constexpr int32_t LeftThumbstickDownLeft{ 22'567 }; // DOWN-LEFT + + // Right thumbstick directions + static constexpr int32_t RightThumbstickUp{ 215 }; // UP + static constexpr int32_t RightThumbstickDown{ 216 }; // DOWN + static constexpr int32_t RightThumbstickRight{ 217 }; // RIGHT + static constexpr int32_t RightThumbstickLeft{ 218 }; // LEFT + + static constexpr int32_t RightThumbstickUpLeft{ 22'580 }; // UP-LEFT + static constexpr int32_t RightThumbstickUpRight{ 22'581 }; //UP-RIGHT + static constexpr int32_t RightThumbstickDownRight{ 22'582 }; // RIGHT-DOWN + static constexpr int32_t RightThumbstickDownLeft{ 22'583 }; // DOWN-LEFT + + // Other buttons + static constexpr int32_t ButtonStart{ 16 }; + static constexpr int32_t ButtonBack{ 32 }; + static constexpr int32_t ButtonShoulderLeft{ 256 }; + static constexpr int32_t ButtonShoulderRight{ 512 }; + static constexpr int32_t ThumbLeftClick{ 64 }; + static constexpr int32_t ThumbRightClick{ 128 }; + + // used internally to denote left or right triggers, similar to the button VKs though they may + // not be used by the OS API state updates in the same way--we virtualize them. + static constexpr int32_t LeftTrigger{ 201 }; + static constexpr int32_t RightTrigger{ 202 }; + + /** + * \brief The button virtual keycodes as a flat array. + */ + static constexpr std::array ButtonCodeArray + { + DpadUp, + DpadDown, + DpadLeft, + DpadRight, + ButtonStart, + ButtonBack, + ThumbLeftClick, + ThumbRightClick, + ButtonShoulderLeft, + ButtonShoulderRight, + ButtonA, + ButtonB, + ButtonX, + ButtonY + }; + + static constexpr int LeftStickDeadzone{ 7849 }; + static constexpr int RightStickDeadzone{ 8689 }; + + static constexpr int LeftTriggerThreshold{ 30 }; + static constexpr int RightTriggerThreshold{ 30 }; + + // The type of the button buffer without const/volatile/reference. + using ButtonBuffer_t = std::remove_reference_t< std::remove_cv_t >; + + // TODO update this if the values become non-const + friend auto hash_value([[maybe_unused]] const KeyboardSettings& obj) -> std::size_t { return 0x0ED35098; } +}; + +static constexpr float MY_PI{ std::numbers::pi_v }; +static constexpr float MY_PI2{ std::numbers::pi_v / float{2} }; +static constexpr float MY_PI8{ std::numbers::pi_v / float{8} }; + +// Primary function template for a directional bounds checker. +template +constexpr auto GetDirection(const float theta) noexcept -> std::optional +{ + return (theta >= Low && theta <= High) ? Dir : std::optional{}; +} + +// This specialization requires custom logic in the bounds check to work. +static constexpr auto specialLow = 7 * MY_PI8; +static constexpr auto specialHigh = 7 * -MY_PI8; + +template<> +constexpr auto GetDirection(const float theta) noexcept -> std::optional +{ + const bool isThetaPositive = theta >= decltype(theta){}; + const bool isWithinBounds = isThetaPositive ? theta >= specialLow : theta <= specialHigh; + return isWithinBounds ? ThumbstickDirection::Left : std::optional{}; +} + +[[nodiscard]] constexpr auto GetDirectionForPolarTheta(const float theta) noexcept -> ThumbstickDirection +{ + const auto dir = GetDirection<-MY_PI8, MY_PI8, ThumbstickDirection::Right>(theta) + .or_else([theta]() { return GetDirection(theta); }) + .or_else([theta]() { return GetDirection<3 * MY_PI8, 5 * MY_PI8, ThumbstickDirection::Up>(theta); }) + .or_else([theta]() { return GetDirection<5 * MY_PI8, 7 * MY_PI8, ThumbstickDirection::LeftUp>(theta); }) + .or_else([theta]() { return GetDirection<7 * MY_PI8, 7 * -MY_PI8, ThumbstickDirection::Left>(theta); }) + .or_else([theta]() { return GetDirection<7 * -MY_PI, 5 * -MY_PI8, ThumbstickDirection::DownLeft>(theta); }) + .or_else([theta]() { return GetDirection<5 * -MY_PI8, 3 * -MY_PI8, ThumbstickDirection::Down>(theta); }) + .or_else([theta]() { return GetDirection<3 * -MY_PI8, -MY_PI8, ThumbstickDirection::RightDown>(theta); }); + return dir.value_or(ThumbstickDirection::Invalid); +} + +[[nodiscard]] constexpr auto GetVirtualKeyFromDirection(const ThumbstickDirection direction, const ControllerStick whichStick) -> std::optional +{ + const bool isLeftStick = whichStick == ControllerStick::LeftStick; + + switch (direction) + { + case ThumbstickDirection::Up: return isLeftStick ? KeyboardSettings::LeftThumbstickUp : KeyboardSettings::RightThumbstickUp; + case ThumbstickDirection::UpRight: return isLeftStick ? KeyboardSettings::LeftThumbstickUpRight : KeyboardSettings::RightThumbstickUpRight; + case ThumbstickDirection::Right: return isLeftStick ? KeyboardSettings::LeftThumbstickRight : KeyboardSettings::RightThumbstickRight; + case ThumbstickDirection::RightDown: return isLeftStick ? KeyboardSettings::LeftThumbstickDownRight : KeyboardSettings::RightThumbstickDownRight; + case ThumbstickDirection::Down: return isLeftStick ? KeyboardSettings::LeftThumbstickDown : KeyboardSettings::RightThumbstickDown; + case ThumbstickDirection::DownLeft: return isLeftStick ? KeyboardSettings::LeftThumbstickDownLeft : KeyboardSettings::RightThumbstickDownLeft; + case ThumbstickDirection::Left: return isLeftStick ? KeyboardSettings::LeftThumbstickLeft : KeyboardSettings::RightThumbstickLeft; + case ThumbstickDirection::LeftUp: return isLeftStick ? KeyboardSettings::LeftThumbstickUpLeft : KeyboardSettings::RightThumbstickUpLeft; + case ThumbstickDirection::Invalid: return {}; + default: + { + throw std::runtime_error("Bad mapping of ThumbstickDirection to virtual key."); + } + } + return {}; +} + +[[nodiscard]] constexpr bool IsFloatZero(const auto testFloat) noexcept +{ + constexpr auto eps = std::numeric_limits::epsilon(); + constexpr auto eps2 = eps * 2; + return std::abs(testFloat) <= eps2; +} + +// [FloatingType, FloatingType] wherein the first member is the polar radius, and the second is the polar theta angle. +using PolarInfoPair = std::pair; +[[nodiscard]] inline auto ComputePolarPair(const float xStickValue, const float yStickValue) noexcept -> PolarInfoPair +{ + constexpr auto nonZeroValue{ std::numeric_limits::min() }; // cannot compute with both values at 0, this is used instead + const bool areBothZero = IsFloatZero(xStickValue) && IsFloatZero(yStickValue); + + const float xValue = areBothZero ? nonZeroValue : xStickValue; + const float yValue = areBothZero ? nonZeroValue : yStickValue; + const auto rad = std::hypot(xValue, yValue); + const auto angle = std::atan2(yValue, xValue); + return { rad, angle }; +} + +/** +* \brief Important helper function to build a small vector of button VKs that are 'down'. Essential function +* is to decompose bit masked state updates into an array. +* \param settingsPack Settings pertaining to deadzone info and virtual keycodes. +* \param controllerState The OS API state update. +* \return small vector of down buttons. +*/ +[[nodiscard]] inline auto GetDownVirtualKeycodesRange( + const std::ranges::range auto& buttonCodeArray, + const auto leftTriggerThresh, + const auto rightTriggerThresh, + const XINPUT_STATE& controllerState) -> sds::SmallVector_t +{ + const auto IsTriggerBeyondThreshold = [](const uint32_t triggerValue, const uint32_t triggerThreshold) noexcept -> bool + { + return triggerValue > triggerThreshold; + }; + + // Keys + sds::SmallVector_t allKeys{}; + for (const auto elem : buttonCodeArray) + { + if (controllerState.Gamepad.wButtons & elem) + allKeys.emplace_back(elem); + } + + // Triggers + if (IsTriggerBeyondThreshold(controllerState.Gamepad.bLeftTrigger, leftTriggerThresh)) + allKeys.emplace_back(KeyboardSettings::LeftTrigger); + if (IsTriggerBeyondThreshold(controllerState.Gamepad.bRightTrigger, rightTriggerThresh)) + allKeys.emplace_back(KeyboardSettings::RightTrigger); + + // Stick axes + constexpr auto LeftStickDz{ KeyboardSettings::LeftStickDeadzone }; + constexpr auto RightStickDz{ KeyboardSettings::RightStickDeadzone }; + + const auto leftThumbstickX{ controllerState.Gamepad.sThumbLX }; + const auto rightThumbstickX{ controllerState.Gamepad.sThumbRX }; + + const auto leftThumbstickY{ controllerState.Gamepad.sThumbLY }; + const auto rightThumbstickY{ controllerState.Gamepad.sThumbRY }; + + const auto leftStickPolarInfo{ ComputePolarPair(leftThumbstickX, leftThumbstickY) }; + const auto rightStickPolarInfo{ ComputePolarPair(rightThumbstickX, rightThumbstickY) }; + + const auto leftDirection{ GetDirectionForPolarTheta(leftStickPolarInfo.second) }; + const auto rightDirection{ GetDirectionForPolarTheta(rightStickPolarInfo.second) }; + + const auto leftThumbstickVk{ GetVirtualKeyFromDirection(leftDirection, ControllerStick::LeftStick) }; + const auto rightThumbstickVk{ GetVirtualKeyFromDirection(rightDirection, ControllerStick::RightStick) }; + + const bool leftIsDown = leftStickPolarInfo.first > LeftStickDz && leftThumbstickVk.has_value(); + const bool rightIsDown = rightStickPolarInfo.first > RightStickDz && rightThumbstickVk.has_value(); + + if (leftIsDown) + allKeys.emplace_back(leftThumbstickVk.value()); + if (rightIsDown) + allKeys.emplace_back(rightThumbstickVk.value()); + + return allKeys; +} + +/** +* \brief Calls the OS API function(s). +* \param playerId Most commonly 0 for a single device connected. +* \return Platform/API specific state structure. +*/ +[[nodiscard]] inline auto GetLegacyApiStateUpdate(const int playerId = 0) noexcept -> XINPUT_STATE +{ + XINPUT_STATE controllerState{}; + XInputGetState(playerId, &controllerState); + return controllerState; +} + +[[nodiscard]] inline auto GetWrappedLegacyApiStateUpdate(const std::ranges::range auto& buttonCodeArray, const int playerId) noexcept -> sds::SmallVector_t +{ + return GetDownVirtualKeycodesRange(buttonCodeArray, KeyboardSettings::LeftTriggerThreshold, KeyboardSettings::RightTriggerThreshold, GetLegacyApiStateUpdate(playerId)); +} + + +inline auto CallSendInput(INPUT* inp, std::uint32_t numSent) noexcept -> UINT +{ + return SendInput(static_cast(numSent), inp, sizeof(INPUT)); +} + +inline void SendMouseMove(const int x, const int y) noexcept +{ + INPUT m_mouseMoveInput{}; + m_mouseMoveInput.type = INPUT_MOUSE; + m_mouseMoveInput.mi.dwFlags = MOUSEEVENTF_MOVE; + + using dx_t = decltype(m_mouseMoveInput.mi.dx); + using dy_t = decltype(m_mouseMoveInput.mi.dy); + m_mouseMoveInput.mi.dx = static_cast(x); + m_mouseMoveInput.mi.dy = -static_cast(y); + m_mouseMoveInput.mi.dwExtraInfo = GetMessageExtraInfo(); + //Finally, send the input + CallSendInput(&m_mouseMoveInput, 1); +} + +// Crude mechanism to keep the loop running until [enter] is pressed. +struct GetterExitCallable final +{ + std::atomic IsDone{ false }; + void GetExitSignal() + { + std::string buf; + std::getline(std::cin, buf); + IsDone.store(true, std::memory_order_relaxed); + } +}; + +auto GetEpochTimestamp() +{ + const auto currentTime = std::chrono::steady_clock::now(); + return std::chrono::duration_cast(currentTime.time_since_epoch()); +} + +auto GetDriverButtonMappings() +{ + using std::vector, sds::MappingContainer, std::cout; + using namespace std::chrono_literals; + using namespace sds; + + constexpr int PadButtonsGroup = 111; // Buttons exclusivity grouping. + constexpr int LeftThumbGroup = 101; // Left thumbstick exclusivity grouping. + const auto PrintMessageAndTime = [](std::string_view msg) + { + std::println("{} @{}", msg, GetEpochTimestamp()); + }; + const auto GetDownLambdaForKeyNamed = [=](const std::string& keyName) + { + return [=]() { PrintMessageAndTime(keyName + "=[DOWN]"); }; + }; + const auto GetUpLambdaForKeyNamed = [=](const std::string& keyName) + { + return [=]() { PrintMessageAndTime(keyName + "=[UP]"); }; + }; + const auto GetRepeatLambdaForKeyNamed = [=](const std::string& keyName) + { + return [=]() { PrintMessageAndTime(keyName + "=[REPEAT]"); }; + }; + const auto GetResetLambdaForKeyNamed = [=](const std::string& keyName) + { + return [=]() { PrintMessageAndTime(keyName + "=[RESET]"); }; + }; + + const auto GetBuiltMapForKeyNamed = [&](const std::string& keyName, const auto virtualKey, const int exGroup, const auto firstDelay) + { + return MappingContainer + { + .OnDown = GetDownLambdaForKeyNamed(keyName), + .OnUp = GetUpLambdaForKeyNamed(keyName), + .OnRepeat = GetRepeatLambdaForKeyNamed(keyName), + .OnReset = GetResetLambdaForKeyNamed(keyName), + .ButtonVirtualKeycode = virtualKey, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = exGroup, + .DelayBeforeFirstRepeat = firstDelay, + //.BetweenRepeatDelay = std::chrono::milliseconds(100) + }; + }; + + KeyboardSettings ksp; + + vector mapBuffer + { + // Pad buttons + GetBuiltMapForKeyNamed("[PAD_A]", ksp.ButtonA, PadButtonsGroup, 500ms), + GetBuiltMapForKeyNamed("[PAD_B]", ksp.ButtonB, PadButtonsGroup, 500ms), + GetBuiltMapForKeyNamed("[PAD_X]", ksp.ButtonX, PadButtonsGroup, 500ms), + GetBuiltMapForKeyNamed("[PAD_Y]", ksp.ButtonY, PadButtonsGroup, 500ms), + // Left thumbstick directional stuff + GetBuiltMapForKeyNamed("[LTHUMB_UP]", ksp.LeftThumbstickUp, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTHUMB_DOWN]", ksp.LeftThumbstickDown, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTHUMB_RIGHT]", ksp.LeftThumbstickRight, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTHUMB_LEFT]", ksp.LeftThumbstickLeft, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTHUMB_DOWN_RIGHT]", ksp.LeftThumbstickDownRight, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTHUMB_DOWN_LEFT]", ksp.LeftThumbstickDownLeft, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTHUMB_UP_RIGHT]", ksp.LeftThumbstickUpRight, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTHUMB_UP_LEFT]", ksp.LeftThumbstickUpLeft, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[LTRIGGER]", ksp.LeftTrigger, LeftThumbGroup, 500ms), + GetBuiltMapForKeyNamed("[RTRIGGER]", ksp.RightTrigger, LeftThumbGroup, 500ms), + // Shoulder buttons + MappingContainer + { + .OnDown = []() { system("cls"); std::cout << "Cleared.\n"; }, + .ButtonVirtualKeycode = ksp.ButtonShoulderRight, + .RepeatingKeyBehavior = sds::RepeatType::None, + }, + MappingContainer + { + .OnDown = []() { /* Add impl for something to do here */ }, + .ButtonVirtualKeycode = ksp.ButtonShoulderLeft, + .RepeatingKeyBehavior = sds::RepeatType::None, + }, + }; + + return mapBuffer; +} + +auto GetDriverMouseMappings() +{ + using std::vector, std::cout; + using namespace std::chrono_literals; + using namespace sds; + constexpr auto FirstDelay = 0ns; // mouse move delays + constexpr auto RepeatDelay = 1200us; + constexpr int MouseExGroup = 102; + + vector mapBuffer + { + // Mouse move stuff + MappingContainer + { + .OnDown = []() + { + SendMouseMove(0, 1); + }, + .OnRepeat = []() + { + SendMouseMove(0, 1); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickUp, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + MappingContainer + { + .OnDown = []() + { + SendMouseMove(1, 1); + }, + .OnRepeat = []() + { + SendMouseMove(1, 1); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickUpRight, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + MappingContainer + { + .OnDown = []() + { + SendMouseMove(-1, 1); + }, + .OnRepeat = []() + { + SendMouseMove(-1, 1); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickUpLeft, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + MappingContainer + { + .OnDown = []() + { + SendMouseMove(0, -1); + }, + .OnRepeat = []() + { + SendMouseMove(0, -1); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickDown, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + MappingContainer + { + .OnDown = []() + { + SendMouseMove(-1, 0); + }, + .OnRepeat = []() + { + SendMouseMove(-1, 0); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickLeft, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + MappingContainer + { + .OnDown = []() + { + SendMouseMove(1, 0); + }, + .OnRepeat = []() + { + SendMouseMove(1, 0); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickRight, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + MappingContainer + { + .OnDown = []() + { + SendMouseMove(1, -1); + }, + .OnRepeat = []() + { + SendMouseMove(1, -1); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickDownRight, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + MappingContainer + { + .OnDown = []() + { + SendMouseMove(-1, -1); + }, + .OnRepeat = []() + { + SendMouseMove(-1, -1); + }, + .ButtonVirtualKeycode = KeyboardSettings::RightThumbstickDownLeft, + .RepeatingKeyBehavior = sds::RepeatType::Infinite, + .ExclusivityGrouping = MouseExGroup, + .DelayBeforeFirstRepeat = FirstDelay, + .BetweenRepeatDelay = RepeatDelay + }, + }; + + return mapBuffer; +} + +inline void TranslationLoop(sds::Translator& translator, sds::OvertakingFilter<>& filter, const std::chrono::nanoseconds sleepDelay) +{ + using namespace std::chrono_literals; + const auto translation = translator(filter(GetWrappedLegacyApiStateUpdate(KeyboardSettings::ButtonCodeArray, 0))); + translation(); + //std::this_thread::sleep_for(sleepDelay); +} + +auto RunTestDriverLoop() +{ + using namespace std::chrono_literals; + + // Building mappings buffer + auto mapBuffer = GetDriverButtonMappings(); + mapBuffer.append_range(GetDriverMouseMappings()); + + std::cout << "Test driver program for XBOX 360 controller (or another XINPUT device.)\n"; + const auto mapBufferSize = mapBuffer.size(); + const auto mappingsSizeInBytes = sizeof(mapBuffer.front()) * mapBuffer.size(); + std::cout << std::vformat("Created mappings buffer with {} mappings. Total size: {} bytes.\n", std::make_format_args(mapBufferSize, mappingsSizeInBytes)); + std::cout << "Starting poll loop for player 0\n"; + + // Mappings are then moved into the translator at construction. + sds::Translator translator{ std::move(mapBuffer) }; + + // The filter is constructed here, to support custom filters with their own construction needs. + sds::OvertakingFilter filter{translator}; + + constexpr auto SleepDelay = std::chrono::nanoseconds{ 1 }; + + GetterExitCallable gec; + const auto exitFuture = std::async(std::launch::async, [&]() { gec.GetExitSignal(); }); + while (!gec.IsDone) + { + TranslationLoop(translator, filter, SleepDelay); + } + std::cout << "Performing cleanup actions...\n"; + const auto cleanupTranslations = translator.GetCleanupActions(); + for (auto& cleanupAction : cleanupTranslations) + cleanupAction(); + + exitFuture.wait(); +} + + +int main() +{ + RunTestDriverLoop(); +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 98a6962..74a5848 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,25 +1,22 @@ -# CMakeList.txt : CMake project for tests, include source and define -# project specific logic here. -# +# CMakeList.txt : CMake project for tests + cmake_minimum_required (VERSION 3.8) +include_directories(../src) + set(TEST_TARGET test_main) -add_executable(${TEST_TARGET} ${TEST_TARGET}.cpp "test_utils.h") +add_executable(${TEST_TARGET} ${TEST_TARGET}.cpp "test_utils.h" ) add_test(NAME ${TEST_TARGET} COMMAND $) -target_include_directories(${TEST_TARGET} PUBLIC . ../src ) - -if (CMAKE_VERSION VERSION_GREATER 3.12) - set_property(TARGET test_main PROPERTY CXX_STANDARD 23) -endif() +target_compile_features(${TEST_TARGET} PUBLIC cxx_std_23) +target_include_directories(${TEST_TARGET} PUBLIC . ../lib_main ) +target_link_libraries(${TEST_TARGET} INTERFACE ${LIBRARY_NAME}) set(TEST_TARGET test_filter) -add_executable(${TEST_TARGET} ${TEST_TARGET}.cpp "test_utils.h") +add_executable(${TEST_TARGET} ${TEST_TARGET}.cpp "test_utils.h" ) add_test(NAME ${TEST_TARGET} COMMAND $) -target_include_directories(${TEST_TARGET} PUBLIC . ../src ) - -if (CMAKE_VERSION VERSION_GREATER 3.12) - set_property(TARGET test_filter PROPERTY CXX_STANDARD 23) -endif() +target_compile_features(${TEST_TARGET} PUBLIC cxx_std_23) +target_include_directories(${TEST_TARGET} PUBLIC . ../lib_main ) +target_link_libraries(${TEST_TARGET} INTERFACE ${LIBRARY_NAME}) set(CMAKE_EXPORT_COMPILE_COMMANDS "on") diff --git a/tests/rand_gen.h b/tests/rand_gen.h index d88da0d..e15fd44 100644 --- a/tests/rand_gen.h +++ b/tests/rand_gen.h @@ -8,198 +8,182 @@ #include #include -/// -/// Implementation for RandomGen. -/// A better random generation utility, as a class object, -/// with min and max distribution value range and templated for string type. -/// (using declaration for standard config). -/// -/// By default, a single byte element string that uses the implementation defined signed-ness of the type 'char' -/// By default, a wide character or otherwise unicode/larger character type that uses the implementation defined signed-ness of the type 'wchar_t' -template -struct RandomGenImpl +using CountType = size_t; +using StringType = std::string; +using WStringType = std::wstring; + +static thread_local std::random_device RandomDevice{}; +static thread_local std::mt19937 RandomElementGenerator{ RandomDevice() }; // seed mersenne engine + +/// Typename of values you want in the container. +/// Distribution template param to use, not less than sizeof an int +/// Helper function used to fill a container +/// some range capable container type +/// the minimum count of the type T in the filled range. +/// the maximum count of the type T in the filled range. +/// minimum integer value used in the distribution. +/// maximum integer value used in the distribution. +/// Throws std::bad_alloc on failure to resize container type. +template + requires std::integral&& std::integral +void DoGenerate(std::ranges::range auto& containerType, + const CountType minLength, + const CountType maxLength, + const T minValue = std::numeric_limits::min(), + const T maxValue = std::numeric_limits::max()) { - using CountType = size_t; - using StringType = StrType; - using WStringType = WStrType; -public: - std::random_device rd; - std::mt19937 randomElementGenerator{ rd() }; // seed mersenne engine -private: - /// Typename of values you want in the container. - /// Distribution template param to use, not less than sizeof an int - /// Helper function used to fill a container - /// some range capable container type - /// the minimum count of the type T in the filled range. - /// the maximum count of the type T in the filled range. - /// minimum integer value used in the distribution. - /// maximum integer value used in the distribution. - /// Throws std::bad_alloc on failure to resize container type. - template - requires std::integral&& std::integral - void DoGenerate(std::ranges::range auto& containerType, - const CountType minLength, - const CountType maxLength, - const T minValue = std::numeric_limits::min(), - const T maxValue = std::numeric_limits::max()) - { - std::uniform_int_distribution distElementPossibility(minValue, maxValue); - std::uniform_int_distribution distLengthPossibility(minLength, maxLength); - //the distribution uses the generator engine to get the value - const auto tLength = static_cast(distLengthPossibility(randomElementGenerator)); - containerType.resize(tLength); // <-- can fail to allocate the memory. - const auto GenLambda = [&]() constexpr { return static_cast(distElementPossibility(randomElementGenerator)); }; - std::ranges::generate(containerType, GenLambda); - } + std::uniform_int_distribution distElementPossibility(minValue, maxValue); + std::uniform_int_distribution distLengthPossibility(minLength, maxLength); + //the distribution uses the generator engine to get the value + const auto tLength = static_cast(distLengthPossibility(RandomElementGenerator)); + containerType.resize(tLength); // <-- can fail to allocate the memory. + const auto GenLambda = [&]() constexpr { return static_cast(distElementPossibility(RandomElementGenerator)); }; + std::ranges::generate(containerType, GenLambda); +} - /// Typename of value you want in the return value. - /// Distribution template param to use, not less than sizeof an int - /// Helper function used to get a range-bound value - /// the minimum value of the type T in the returned value. - /// the maximum value of the type T in the returned value. - /// T with random value - template requires std::integral&& std::integral - [[nodiscard]] T DoGenerateSingle(const T minValue, const T maxValue) noexcept - { - std::uniform_int_distribution distElementPossibility(minValue, maxValue); - //the distribution uses the generator engine to get the value - return static_cast(distElementPossibility(randomElementGenerator)); - } -public: - /// Returns a vector of a random number of type T with randomized content using a uniform distribution. T must be default constructable. - /// the maximum count of the type T in the returned vector. - /// the minimum count of the type T in the returned vector. - /// a vector of type T with randomized content. Empty vector on error. - template - requires std::integral && (!std::same_as) - [[nodiscard]] auto BuildRandomVector(const CountType minLength, const CountType maxLength) -> std::vector - { - //arg error checking, returns empty vector as per description - if (minLength > maxLength || (maxLength <= 0) || (minLength <= 0)) - { - return std::vector(); - } - std::vector currentBuiltVector; - if constexpr (sizeof(T) <= sizeof(int) && std::unsigned_integral) - { - DoGenerate(currentBuiltVector, minLength, maxLength); - } - else if constexpr (sizeof(T) <= sizeof(int) && std::signed_integral) - { - DoGenerate(currentBuiltVector, minLength, maxLength); - } - else - { - DoGenerate(currentBuiltVector, minLength, maxLength); - } - return currentBuiltVector; - } +/// Typename of value you want in the return value. +/// Distribution template param to use, not less than sizeof an int +/// Helper function used to get a range-bound value +/// the minimum value of the type T in the returned value. +/// the maximum value of the type T in the returned value. +/// T with random value +template requires std::integral&& std::integral +[[nodiscard]] T DoGenerateSingle(const T minValue, const T maxValue) noexcept +{ + std::uniform_int_distribution distElementPossibility(minValue, maxValue); + //the distribution uses the generator engine to get the value + return static_cast(distElementPossibility(RandomElementGenerator)); +} - /// Fills a container with type T storing randomized values using a uniform distribution. Primitive type T must be default constructable. - /// some range capable container type - /// the minimum count of the type T in the returned vector. - /// the maximum count of the type T in the returned vector. - /// true on success, false on error. - template requires std::integral && (!std::same_as) - [[nodiscard]] bool FillContainerRandom(std::ranges::range auto& containerType, const CountType minLength, const CountType maxLength) - { - //arg error checking, returns false as per description - if (minLength > maxLength || (maxLength <= 0) || (minLength <= 0)) - { - return false; - } - if constexpr (sizeof(T) <= sizeof(int) && std::unsigned_integral) - { - DoGenerate(containerType, minLength, maxLength); - } - else if constexpr (sizeof(T) <= sizeof(int) && std::signed_integral) - { - DoGenerate(containerType, minLength, maxLength); - } - else - { - DoGenerate(containerType, minLength, maxLength); - } - return true; +/// Returns a vector of a random number of type T with randomized content using a uniform distribution. T must be default constructable. +/// the maximum count of the type T in the returned vector. +/// the minimum count of the type T in the returned vector. +/// a vector of type T with randomized content. Empty vector on error. +template +requires std::integral && (!std::same_as) +[[nodiscard]] auto BuildRandomVector(const CountType minLength, const CountType maxLength) -> std::vector +{ + //arg error checking, returns empty vector as per description + if (minLength > maxLength || (maxLength <= 0) || (minLength <= 0)) + { + return std::vector(); + } + std::vector currentBuiltVector; + if constexpr (sizeof(T) <= sizeof(int) && std::unsigned_integral) + { + DoGenerate(currentBuiltVector, minLength, maxLength); + } + else if constexpr (sizeof(T) <= sizeof(int) && std::signed_integral) + { + DoGenerate(currentBuiltVector, minLength, maxLength); + } + else + { + DoGenerate(currentBuiltVector, minLength, maxLength); } + return currentBuiltVector; +} - /// Returns a vector of StringType with randomized content using a uniform distribution.NOTE: - /// The char type is a character representation type that efficiently encodes members of the basic execution character set. - /// The C++ compiler treats variables of type char, signed char, and unsigned char as having different types. - /// ***Microsoft - specific: Variables of type char are promoted to int as if from type signed char by default, - /// unless the / J compilation option is used. In this case, they're treated as type unsigned char and are promoted to int without sign extension.*** - /// MSDOCS - /// the number of strings in the returned vector. - /// the minimum length of the strings in the returned vector. - /// the maximum length of the strings in the returned vector. - /// a std::vector of StringType with randomized content. Empty vector on error. - [[nodiscard]] auto BuildRandomStringVector(const CountType numberOfStrings, const CountType minLength, const CountType maxLength) -> std::vector - { - //arg error checking, returns empty vector as per description - if (minLength > maxLength || (maxLength <= 0) || (numberOfStrings <= 0) || (minLength <= 0)) - { - return {}; - } - std::vector ret{}; - ret.reserve(numberOfStrings); - for (CountType i = 0; i < numberOfStrings; i++) - { - const auto tempCharVector = RandomGenImpl::BuildRandomVector(minLength, maxLength); - ret.emplace_back(StringType{ tempCharVector.begin(), tempCharVector.end() }); - } - return ret; +/// Fills a container with type T storing randomized values using a uniform distribution. Primitive type T must be default constructable. +/// some range capable container type +/// the minimum count of the type T in the returned vector. +/// the maximum count of the type T in the returned vector. +/// true on success, false on error. +template requires std::integral && (!std::same_as) +[[nodiscard]] bool FillContainerRandom(std::ranges::range auto& containerType, const CountType minLength, const CountType maxLength) +{ + //arg error checking, returns false as per description + if (minLength > maxLength || (maxLength <= 0) || (minLength <= 0)) + { + return false; } + if constexpr (sizeof(T) <= sizeof(int) && std::unsigned_integral) + { + DoGenerate(containerType, minLength, maxLength); + } + else if constexpr (sizeof(T) <= sizeof(int) && std::signed_integral) + { + DoGenerate(containerType, minLength, maxLength); + } + else + { + DoGenerate(containerType, minLength, maxLength); + } + return true; +} - /// Returns a vector of WStringType with randomized content using a uniform distribution. - /// the number of strings in the returned vector. - /// the minimum length of the strings in the returned vector. - /// the maximum length of the strings in the returned vector. - /// a vector of WStringType with randomized content. Empty vector on error. - [[nodiscard]] auto BuildRandomWStringVector(const CountType numberOfStrings, const CountType minLength, const CountType maxLength) - { - //arg error checking, returns empty vector as per description - if (minLength > maxLength || (maxLength <= 0) || (numberOfStrings <= 0) || (minLength <= 0)) - { - return std::vector(); - } - std::vector ret; - ret.reserve(numberOfStrings); - for (CountType i = 0; i < numberOfStrings; i++) - { - const auto tempString = RandomGenImpl::BuildRandomVector(minLength, maxLength); - ret.emplace_back(WStringType{ tempString.begin(), tempString.end() }); - } - return ret; +/// Returns a vector of StringType with randomized content using a uniform distribution.NOTE: +/// The char type is a character representation type that efficiently encodes members of the basic execution character set. +/// The C++ compiler treats variables of type char, signed char, and unsigned char as having different types. +/// ***Microsoft - specific: Variables of type char are promoted to int as if from type signed char by default, +/// unless the / J compilation option is used. In this case, they're treated as type unsigned char and are promoted to int without sign extension.*** +/// MSDOCS +/// the number of strings in the returned vector. +/// the minimum length of the strings in the returned vector. +/// the maximum length of the strings in the returned vector. +/// a std::vector of StringType with randomized content. Empty vector on error. +[[nodiscard]] auto BuildRandomStringVector(const CountType numberOfStrings, const CountType minLength, const CountType maxLength) -> std::vector +{ + //arg error checking, returns empty vector as per description + if (minLength > maxLength || (maxLength <= 0) || (numberOfStrings <= 0) || (minLength <= 0)) + { + return {}; + } + std::vector ret{}; + ret.reserve(numberOfStrings); + for (CountType i = 0; i < numberOfStrings; i++) + { + const auto tempCharVector = BuildRandomVector(minLength, maxLength); + ret.emplace_back(StringType{ tempCharVector.begin(), tempCharVector.end() }); } + return ret; +} - /// Returns a vector of a random number of type T with randomized content using a uniform distribution. T must be default constructable. - /// the minimum value of the type T in the returned value. - /// the maximum value of the type T in the returned value. - /// A type T with random value. Default constructed T on error. - template requires std::integral && (!std::same_as) - [[nodiscard]] T BuildRandomSingleValue(const T minValue = std::numeric_limits::min(), const T maxValue = std::numeric_limits::max()) noexcept - { - //arg error checking, returns default constructed T as per description - if (minValue > maxValue) - { - return T{}; - } - if constexpr (sizeof(T) <= sizeof(int) && std::unsigned_integral) - { - return DoGenerateSingle(minValue, maxValue); - } - else if constexpr (sizeof(T) <= sizeof(int) && std::signed_integral) - { - return DoGenerateSingle(minValue, maxValue); - } - else - { - return DoGenerateSingle(minValue, maxValue); - } - } -}; +/// Returns a vector of WStringType with randomized content using a uniform distribution. +/// the number of strings in the returned vector. +/// the minimum length of the strings in the returned vector. +/// the maximum length of the strings in the returned vector. +/// a vector of WStringType with randomized content. Empty vector on error. +[[nodiscard]] auto BuildRandomWStringVector(const CountType numberOfStrings, const CountType minLength, const CountType maxLength) +{ + //arg error checking, returns empty vector as per description + if (minLength > maxLength || (maxLength <= 0) || (numberOfStrings <= 0) || (minLength <= 0)) + { + return std::vector(); + } + std::vector ret; + ret.reserve(numberOfStrings); + for (CountType i = 0; i < numberOfStrings; i++) + { + const auto tempString = BuildRandomVector(minLength, maxLength); + ret.emplace_back(WStringType{ tempString.begin(), tempString.end() }); + } + return ret; +} -/// -/// Using declaration for standard config. -/// -using RandomGen = RandomGenImpl<>; \ No newline at end of file +/// Returns a random number of type T using a uniform distribution. T must be default constructable. +/// the minimum value of the type T in the returned value. +/// the maximum value of the type T in the returned value. +/// A type T with random value. Default constructed T on error. +template + requires std::integral && (!std::same_as) +[[nodiscard]] T BuildRandomSingleValue(const T minValue = std::numeric_limits::min(), const T maxValue = std::numeric_limits::max()) noexcept +{ + //arg error checking, returns default constructed T as per description + if (minValue > maxValue) + { + return T{}; + } + if constexpr (sizeof(T) <= sizeof(int) && std::unsigned_integral) + { + return DoGenerateSingle(minValue, maxValue); + } + else if constexpr (sizeof(T) <= sizeof(int) && std::signed_integral) + { + return DoGenerateSingle(minValue, maxValue); + } + else + { + return DoGenerateSingle(minValue, maxValue); + } +} \ No newline at end of file diff --git a/tests/test_filter.cpp b/tests/test_filter.cpp index 17f0d36..4ff4305 100644 --- a/tests/test_filter.cpp +++ b/tests/test_filter.cpp @@ -5,9 +5,11 @@ struct FilterFixture { - std::vector Mappings{ GetTestDriverMappings() }; + using Vector_t = std::vector; + using Maps_t = std::shared_ptr; + Maps_t Mappings{ std::make_shared(GetTestDriverMappings()) }; - auto GetMappingsRange() const -> const std::vector& + auto GetMappingsRange() const -> const Maps_t { return Mappings; } @@ -33,8 +35,11 @@ int TestFilter() using namespace std::chrono_literals; using namespace std::chrono; - FilterFixture fixt; - sds::OvertakingFilter filt{ fixt }; + auto mappings = GetTestDriverMappings(); + + FilterFixture translator; + //sds::Translator translator{ GetTestDriverMappings() }; + sds::OvertakingFilter filt{ translator }; // Begin clock start const auto startTime = steady_clock::now(); @@ -75,7 +80,8 @@ int TestFilter() const auto totalTime = steady_clock::now() - startTime; - std::cout << std::vformat("Total time: {}\n", std::make_format_args(duration_cast(totalTime))).c_str(); + const auto durationTotalTime = duration_cast(totalTime); + std::cout << std::vformat("Total time: {}\n", std::make_format_args(durationTotalTime)).c_str(); return 0; } diff --git a/tests/test_main.cpp b/tests/test_main.cpp index da1f8fe..b2e3b8b 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -38,7 +38,7 @@ auto RunTestLoop(auto& translator, auto& filter, auto dataSet, const size_t Data // Run test data set for (auto data : dataSet) { - translator(filter(std::move(data)))(); + translator(filter(data))(); } // Compute times diff --git a/tests/test_utils.h b/tests/test_utils.h index 2e60de0..d61bb25 100644 --- a/tests/test_utils.h +++ b/tests/test_utils.h @@ -48,21 +48,8 @@ auto GetPrintDurationsString(const auto dur) -> std::string return ss.str(); } -auto GetPrintBehaviorsForKey(const std::string keyName) -> sds::KeyStateBehaviors -{ - using std::cout; - sds::KeyStateBehaviors behaviors - { - //.OnDown = [=]() { cout << "[" << keyName << "]-[OnDown]\n"; }, - //.OnUp = [=]() { cout << "[" << keyName << "]-[OnUp]\n"; }, - //.OnRepeat = [=]() { cout << "[" << keyName << "]-[OnRepeat]\n"; }, - //.OnReset = [=]() { cout << "[" << keyName << "]-[OnReset]\n"; } - }; - return behaviors; -} - // Mappings buffer for the test driver, with a single group. -auto GetTestDriverMappings(size_t count = 100, const int beginId = 1) -> std::vector +auto GetTestDriverMappings(size_t count = 32, const int beginId = 1) -> std::vector { using namespace sds; using std::cout, std::println; @@ -75,8 +62,10 @@ auto GetTestDriverMappings(size_t count = 100, const int beginId = 1) -> std::ve { return MappingContainer { - ButtonDescription{i, {}, Grouping}, - KeyStateBehaviors{GetPrintBehaviorsForKey(std::to_string(i))} + .ButtonVirtualKeycode = i, + .ExclusivityGrouping = Grouping, + .DelayBeforeFirstRepeat = 0s, + .BetweenRepeatDelay = 0s, }; }); @@ -86,7 +75,7 @@ auto GetTestDriverMappings(size_t count = 100, const int beginId = 1) -> std::ve // Range of mapping IDs, from the mappings buffer. auto GetMappingIdRange(const std::vector& mappings) -> std::vector { - auto idRange = mappings | std::views::transform([](const sds::MappingContainer& m) { return m.Button.ButtonVirtualKeycode; }); + auto idRange = mappings | std::views::transform([](const sds::MappingContainer& m) { return m.ButtonVirtualKeycode; }); return std::ranges::to>(idRange); } @@ -95,7 +84,6 @@ auto GetInputSequence(const std::vector& mappings, size_t { const auto mappingIdsView = GetMappingIdRange(mappings); - RandomGen rander; std::vector> dataSet; dataSet.reserve(count); @@ -105,7 +93,7 @@ auto GetInputSequence(const std::vector& mappings, size_t //const auto rangeBegin = rander.BuildRandomSingleValue(1, (int)mappingIdsView.size() / 2); //auto mappingIds = std::vector(mappingIdsView.cbegin(), mappingIdsView.cbegin() + rangeBegin); auto mappingIds = std::vector(mappingIdsView.cbegin(), mappingIdsView.cend()); - std::ranges::shuffle(mappingIds, rander.randomElementGenerator); + std::ranges::shuffle(mappingIds, RandomElementGenerator); dataSet.emplace_back(std::move(mappingIds)); } return dataSet; @@ -136,11 +124,11 @@ auto GetBuiltFilter(const auto& translator) auto getMappingWithId(int id, int group) { using namespace std::chrono_literals; - return sds::MappingContainer + return sds::MappingContainer { - sds::ButtonDescription{id, {}, group}, - sds::KeyStateBehaviors{}, - 0s, - 0s + .ButtonVirtualKeycode = id, + .ExclusivityGrouping = group, + .DelayBeforeFirstRepeat = 0s, + .BetweenRepeatDelay = 0s, }; } \ No newline at end of file