summaryrefslogtreecommitdiffstats
path: root/xbmc/input/joysticks/generic
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
commitc04dcc2e7d834218ef2d4194331e383402495ae1 (patch)
tree7333e38d10d75386e60f336b80c2443c1166031d /xbmc/input/joysticks/generic
parentInitial commit. (diff)
downloadkodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz
kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/input/joysticks/generic')
-rw-r--r--xbmc/input/joysticks/generic/ButtonMapping.cpp592
-rw-r--r--xbmc/input/joysticks/generic/ButtonMapping.h393
-rw-r--r--xbmc/input/joysticks/generic/CMakeLists.txt11
-rw-r--r--xbmc/input/joysticks/generic/DriverReceiving.cpp38
-rw-r--r--xbmc/input/joysticks/generic/DriverReceiving.h46
-rw-r--r--xbmc/input/joysticks/generic/FeatureHandling.cpp551
-rw-r--r--xbmc/input/joysticks/generic/FeatureHandling.h282
-rw-r--r--xbmc/input/joysticks/generic/InputHandling.cpp179
-rw-r--r--xbmc/input/joysticks/generic/InputHandling.h69
9 files changed, 2161 insertions, 0 deletions
diff --git a/xbmc/input/joysticks/generic/ButtonMapping.cpp b/xbmc/input/joysticks/generic/ButtonMapping.cpp
new file mode 100644
index 0000000..665a233
--- /dev/null
+++ b/xbmc/input/joysticks/generic/ButtonMapping.cpp
@@ -0,0 +1,592 @@
+/*
+ * Copyright (C) 2014-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ButtonMapping.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/IKeymap.h"
+#include "input/InputTranslator.h"
+#include "input/Key.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IButtonMapper.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <cmath>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+#define MAPPING_COOLDOWN_MS 50 // Guard against repeated input
+#define AXIS_THRESHOLD 0.75f // Axis must exceed this value to be mapped
+#define TRIGGER_DELAY_MS \
+ 200 // Delay trigger detection to handle anomalous triggers with non-zero center
+
+// --- CPrimitiveDetector ------------------------------------------------------
+
+CPrimitiveDetector::CPrimitiveDetector(CButtonMapping* buttonMapping)
+ : m_buttonMapping(buttonMapping)
+{
+}
+
+bool CPrimitiveDetector::MapPrimitive(const CDriverPrimitive& primitive)
+{
+ if (primitive.IsValid())
+ return m_buttonMapping->MapPrimitive(primitive);
+
+ return false;
+}
+
+// --- CButtonDetector ---------------------------------------------------------
+
+CButtonDetector::CButtonDetector(CButtonMapping* buttonMapping, unsigned int buttonIndex)
+ : CPrimitiveDetector(buttonMapping), m_buttonIndex(buttonIndex)
+{
+}
+
+bool CButtonDetector::OnMotion(bool bPressed)
+{
+ if (bPressed)
+ return MapPrimitive(CDriverPrimitive(PRIMITIVE_TYPE::BUTTON, m_buttonIndex));
+
+ return false;
+}
+
+// --- CHatDetector ------------------------------------------------------------
+
+CHatDetector::CHatDetector(CButtonMapping* buttonMapping, unsigned int hatIndex)
+ : CPrimitiveDetector(buttonMapping), m_hatIndex(hatIndex)
+{
+}
+
+bool CHatDetector::OnMotion(HAT_STATE state)
+{
+ return MapPrimitive(CDriverPrimitive(m_hatIndex, static_cast<HAT_DIRECTION>(state)));
+}
+
+// --- CAxisDetector -----------------------------------------------------------
+
+CAxisDetector::CAxisDetector(CButtonMapping* buttonMapping,
+ unsigned int axisIndex,
+ const AxisConfiguration& config)
+ : CPrimitiveDetector(buttonMapping),
+ m_axisIndex(axisIndex),
+ m_config(config),
+ m_state(AXIS_STATE::INACTIVE),
+ m_type(AXIS_TYPE::UNKNOWN),
+ m_initialPositionKnown(false),
+ m_initialPosition(0.0f),
+ m_initialPositionChanged(false)
+{
+}
+
+bool CAxisDetector::OnMotion(float position)
+{
+ DetectType(position);
+
+ if (m_type != AXIS_TYPE::UNKNOWN)
+ {
+ // Update position if this axis is an anomalous trigger
+ if (m_type == AXIS_TYPE::OFFSET)
+ position = (position - m_config.center) / m_config.range;
+
+ // Reset state if position crosses zero
+ if (m_state == AXIS_STATE::MAPPED)
+ {
+ SEMIAXIS_DIRECTION activatedDir = m_activatedPrimitive.SemiAxisDirection();
+ SEMIAXIS_DIRECTION newDir = CJoystickTranslator::PositionToSemiAxisDirection(position);
+
+ if (activatedDir != newDir)
+ m_state = AXIS_STATE::INACTIVE;
+ }
+
+ // Check if axis has become activated
+ if (m_state == AXIS_STATE::INACTIVE)
+ {
+ if (std::abs(position) >= AXIS_THRESHOLD)
+ m_state = AXIS_STATE::ACTIVATED;
+
+ if (m_state == AXIS_STATE::ACTIVATED)
+ {
+ // Range is set later for anomalous triggers
+ m_activatedPrimitive =
+ CDriverPrimitive(m_axisIndex, m_config.center,
+ CJoystickTranslator::PositionToSemiAxisDirection(position), 1);
+ m_activationTimeMs = std::chrono::steady_clock::now();
+ }
+ }
+ }
+
+ return true;
+}
+
+void CAxisDetector::ProcessMotion()
+{
+ // Process newly-activated axis
+ if (m_state == AXIS_STATE::ACTIVATED)
+ {
+ // Ignore anomalous triggers for a bit so we can detect the full range
+ bool bIgnore = false;
+ if (m_type == AXIS_TYPE::OFFSET)
+ {
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - m_activationTimeMs);
+
+ if (duration.count() < TRIGGER_DELAY_MS)
+ bIgnore = true;
+ }
+
+ if (!bIgnore)
+ {
+ // Update driver primitive's range if we're mapping an anomalous trigger
+ if (m_type == AXIS_TYPE::OFFSET)
+ {
+ m_activatedPrimitive =
+ CDriverPrimitive(m_activatedPrimitive.Index(), m_activatedPrimitive.Center(),
+ m_activatedPrimitive.SemiAxisDirection(), m_config.range);
+ }
+
+ // Map primitive
+ if (!MapPrimitive(m_activatedPrimitive))
+ {
+ if (m_type == AXIS_TYPE::OFFSET)
+ CLog::Log(LOGDEBUG, "Mapping offset axis {} failed", m_axisIndex);
+ else
+ CLog::Log(LOGDEBUG, "Mapping normal axis {} failed", m_axisIndex);
+ }
+
+ m_state = AXIS_STATE::MAPPED;
+ }
+ }
+}
+
+void CAxisDetector::SetEmitted(const CDriverPrimitive& activePrimitive)
+{
+ m_state = AXIS_STATE::MAPPED;
+ m_activatedPrimitive = activePrimitive;
+}
+
+void CAxisDetector::DetectType(float position)
+{
+ // Some platforms don't report a value until the axis is first changed.
+ // Detection relies on an initial value, so this axis will be disabled until
+ // the user begins button mapping again.
+ if (m_config.bLateDiscovery)
+ return;
+
+ // Update range if a range of > 1 is observed
+ if (std::abs(position - m_config.center) > 1.0f)
+ m_config.range = 2;
+
+ if (m_type != AXIS_TYPE::UNKNOWN)
+ return;
+
+ if (m_config.bKnown)
+ {
+ if (m_config.center == 0)
+ m_type = AXIS_TYPE::NORMAL;
+ else
+ m_type = AXIS_TYPE::OFFSET;
+ }
+
+ if (m_type != AXIS_TYPE::UNKNOWN)
+ return;
+
+ if (!m_initialPositionKnown)
+ {
+ m_initialPositionKnown = true;
+ m_initialPosition = position;
+ }
+
+ if (position != m_initialPosition)
+ m_initialPositionChanged = true;
+
+ if (m_initialPositionChanged)
+ {
+ // Calculate center based on initial position.
+ if (m_initialPosition < -0.5f)
+ {
+ m_config.center = -1;
+ m_type = AXIS_TYPE::OFFSET;
+ CLog::Log(LOGDEBUG, "Anomalous trigger detected on axis {} with center {}", m_axisIndex,
+ m_config.center);
+ }
+ else if (m_initialPosition > 0.5f)
+ {
+ m_config.center = 1;
+ m_type = AXIS_TYPE::OFFSET;
+ CLog::Log(LOGDEBUG, "Anomalous trigger detected on axis {} with center {}", m_axisIndex,
+ m_config.center);
+ }
+ else
+ {
+ m_type = AXIS_TYPE::NORMAL;
+ CLog::Log(LOGDEBUG, "Normal axis detected on axis {}", m_axisIndex);
+ }
+ }
+}
+
+// --- CKeyDetector ---------------------------------------------------------
+
+CKeyDetector::CKeyDetector(CButtonMapping* buttonMapping, XBMCKey keycode)
+ : CPrimitiveDetector(buttonMapping), m_keycode(keycode)
+{
+}
+
+bool CKeyDetector::OnMotion(bool bPressed)
+{
+ if (bPressed)
+ return MapPrimitive(CDriverPrimitive(m_keycode));
+
+ return false;
+}
+
+// --- CMouseButtonDetector ----------------------------------------------------
+
+CMouseButtonDetector::CMouseButtonDetector(CButtonMapping* buttonMapping,
+ MOUSE::BUTTON_ID buttonIndex)
+ : CPrimitiveDetector(buttonMapping), m_buttonIndex(buttonIndex)
+{
+}
+
+bool CMouseButtonDetector::OnMotion(bool bPressed)
+{
+ if (bPressed)
+ return MapPrimitive(CDriverPrimitive(m_buttonIndex));
+
+ return false;
+}
+
+// --- CPointerDetector --------------------------------------------------------
+
+CPointerDetector::CPointerDetector(CButtonMapping* buttonMapping)
+ : CPrimitiveDetector(buttonMapping)
+{
+}
+
+bool CPointerDetector::OnMotion(int x, int y)
+{
+ if (!m_bStarted)
+ {
+ m_bStarted = true;
+ m_startX = x;
+ m_startY = y;
+ m_frameCount = 0;
+ }
+
+ if (m_frameCount++ >= MIN_FRAME_COUNT)
+ {
+ int dx = x - m_startX;
+ int dy = y - m_startY;
+
+ INPUT::INTERCARDINAL_DIRECTION dir = GetPointerDirection(dx, dy);
+
+ CDriverPrimitive primitive(static_cast<RELATIVE_POINTER_DIRECTION>(dir));
+ if (primitive.IsValid())
+ {
+ if (MapPrimitive(primitive))
+ m_bStarted = false;
+ }
+ }
+
+ return true;
+}
+
+KODI::INPUT::INTERCARDINAL_DIRECTION CPointerDetector::GetPointerDirection(int x, int y)
+{
+ using namespace INPUT;
+
+ // Translate from left-handed coordinate system to right-handed coordinate system
+ y *= -1;
+
+ return CInputTranslator::VectorToIntercardinalDirection(static_cast<float>(x),
+ static_cast<float>(y));
+}
+
+// --- CButtonMapping ----------------------------------------------------------
+
+CButtonMapping::CButtonMapping(IButtonMapper* buttonMapper, IButtonMap* buttonMap, IKeymap* keymap)
+ : m_buttonMapper(buttonMapper), m_buttonMap(buttonMap), m_keymap(keymap), m_frameCount(0)
+{
+ assert(m_buttonMapper != nullptr);
+ assert(m_buttonMap != nullptr);
+
+ // Make sure axes mapped to Select are centered before they can be mapped.
+ // This ensures that they are not immediately mapped to the first button.
+ if (m_keymap)
+ {
+ using namespace GAME;
+
+ CControllerManager& controllerManager = CServiceBroker::GetGameControllerManager();
+ ControllerPtr controller = controllerManager.GetController(m_keymap->ControllerID());
+
+ const auto& features = controller->Features();
+ for (const auto& feature : features)
+ {
+ bool bIsSelectAction = false;
+
+ const auto& actions =
+ m_keymap->GetActions(CJoystickUtils::MakeKeyName(feature.Name())).actions;
+ if (!actions.empty() && actions.begin()->actionId == ACTION_SELECT_ITEM)
+ bIsSelectAction = true;
+
+ if (!bIsSelectAction)
+ continue;
+
+ CDriverPrimitive primitive;
+ if (!m_buttonMap->GetScalar(feature.Name(), primitive))
+ continue;
+
+ if (primitive.Type() != PRIMITIVE_TYPE::SEMIAXIS)
+ continue;
+
+ // Set initial config, as detection will fail because axis is already activated
+ AxisConfiguration axisConfig;
+ axisConfig.bKnown = true;
+ axisConfig.center = primitive.Center();
+ axisConfig.range = primitive.Range();
+
+ GetAxis(primitive.Index(), static_cast<float>(primitive.Center()), axisConfig)
+ .SetEmitted(primitive);
+ }
+ }
+}
+
+bool CButtonMapping::OnButtonMotion(unsigned int buttonIndex, bool bPressed)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::BUTTON))
+ return false;
+
+ return GetButton(buttonIndex).OnMotion(bPressed);
+}
+
+bool CButtonMapping::OnHatMotion(unsigned int hatIndex, HAT_STATE state)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::HAT))
+ return false;
+
+ return GetHat(hatIndex).OnMotion(state);
+}
+
+bool CButtonMapping::OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::SEMIAXIS))
+ return false;
+
+ return GetAxis(axisIndex, position).OnMotion(position);
+}
+
+void CButtonMapping::OnInputFrame(void)
+{
+ for (auto& axis : m_axes)
+ axis.second.ProcessMotion();
+
+ m_buttonMapper->OnEventFrame(m_buttonMap, IsMapping());
+
+ m_frameCount++;
+}
+
+bool CButtonMapping::OnKeyPress(const CKey& key)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::KEY))
+ return false;
+
+ return GetKey(static_cast<XBMCKey>(key.GetKeycode())).OnMotion(true);
+}
+
+bool CButtonMapping::OnPosition(int x, int y)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::RELATIVE_POINTER))
+ return false;
+
+ return GetPointer().OnMotion(x, y);
+}
+
+bool CButtonMapping::OnButtonPress(MOUSE::BUTTON_ID button)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::MOUSE_BUTTON))
+ return false;
+
+ return GetMouseButton(button).OnMotion(true);
+}
+
+void CButtonMapping::OnButtonRelease(MOUSE::BUTTON_ID button)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::MOUSE_BUTTON))
+ return;
+
+ GetMouseButton(button).OnMotion(false);
+}
+
+void CButtonMapping::SaveButtonMap()
+{
+ m_buttonMap->SaveButtonMap();
+}
+
+void CButtonMapping::ResetIgnoredPrimitives()
+{
+ std::vector<CDriverPrimitive> empty;
+ m_buttonMap->SetIgnoredPrimitives(empty);
+}
+
+void CButtonMapping::RevertButtonMap()
+{
+ m_buttonMap->RevertButtonMap();
+}
+
+bool CButtonMapping::MapPrimitive(const CDriverPrimitive& primitive)
+{
+ bool bHandled = false;
+
+ if (m_buttonMap->IsIgnored(primitive))
+ {
+ bHandled = true;
+ }
+ else
+ {
+ auto now = std::chrono::steady_clock::now();
+
+ bool bTimeoutElapsed = true;
+
+ if (m_buttonMapper->NeedsCooldown())
+ bTimeoutElapsed = (now >= m_lastAction + std::chrono::milliseconds(MAPPING_COOLDOWN_MS));
+
+ if (bTimeoutElapsed)
+ {
+ bHandled = m_buttonMapper->MapPrimitive(m_buttonMap, m_keymap, primitive);
+
+ if (bHandled)
+ m_lastAction = std::chrono::steady_clock::now();
+ }
+ else
+ {
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastAction);
+
+ CLog::Log(LOGDEBUG, "Button mapping: rapid input after {}ms dropped for profile \"{}\"",
+ duration.count(), m_buttonMapper->ControllerID());
+ bHandled = true;
+ }
+ }
+
+ return bHandled;
+}
+
+bool CButtonMapping::IsMapping() const
+{
+ for (auto itAxis : m_axes)
+ {
+ if (itAxis.second.IsMapping())
+ return true;
+ }
+
+ return false;
+}
+
+CButtonDetector& CButtonMapping::GetButton(unsigned int buttonIndex)
+{
+ auto itButton = m_buttons.find(buttonIndex);
+
+ if (itButton == m_buttons.end())
+ {
+ m_buttons.insert(std::make_pair(buttonIndex, CButtonDetector(this, buttonIndex)));
+ itButton = m_buttons.find(buttonIndex);
+ }
+
+ return itButton->second;
+}
+
+CHatDetector& CButtonMapping::GetHat(unsigned int hatIndex)
+{
+ auto itHat = m_hats.find(hatIndex);
+
+ if (itHat == m_hats.end())
+ {
+ m_hats.insert(std::make_pair(hatIndex, CHatDetector(this, hatIndex)));
+ itHat = m_hats.find(hatIndex);
+ }
+
+ return itHat->second;
+}
+
+CAxisDetector& CButtonMapping::GetAxis(
+ unsigned int axisIndex,
+ float position,
+ const AxisConfiguration& initialConfig /* = AxisConfiguration() */)
+{
+ auto itAxis = m_axes.find(axisIndex);
+
+ if (itAxis == m_axes.end())
+ {
+ AxisConfiguration config(initialConfig);
+
+ if (m_frameCount >= 2)
+ {
+ config.bLateDiscovery = true;
+ OnLateDiscovery(axisIndex);
+ }
+
+ // Report axis
+ CLog::Log(LOGDEBUG, "Axis {} discovered at position {:.4f} after {} frames", axisIndex,
+ position, static_cast<unsigned long>(m_frameCount));
+
+ m_axes.insert(std::make_pair(axisIndex, CAxisDetector(this, axisIndex, config)));
+ itAxis = m_axes.find(axisIndex);
+ }
+
+ return itAxis->second;
+}
+
+CKeyDetector& CButtonMapping::GetKey(XBMCKey keycode)
+{
+ auto itKey = m_keys.find(keycode);
+
+ if (itKey == m_keys.end())
+ {
+ m_keys.insert(std::make_pair(keycode, CKeyDetector(this, keycode)));
+ itKey = m_keys.find(keycode);
+ }
+
+ return itKey->second;
+}
+
+CMouseButtonDetector& CButtonMapping::GetMouseButton(MOUSE::BUTTON_ID buttonIndex)
+{
+ auto itButton = m_mouseButtons.find(buttonIndex);
+
+ if (itButton == m_mouseButtons.end())
+ {
+ m_mouseButtons.insert(std::make_pair(buttonIndex, CMouseButtonDetector(this, buttonIndex)));
+ itButton = m_mouseButtons.find(buttonIndex);
+ }
+
+ return itButton->second;
+}
+
+CPointerDetector& CButtonMapping::GetPointer()
+{
+ if (!m_pointer)
+ m_pointer.reset(new CPointerDetector(this));
+
+ return *m_pointer;
+}
+
+void CButtonMapping::OnLateDiscovery(unsigned int axisIndex)
+{
+ m_buttonMapper->OnLateAxis(m_buttonMap, axisIndex);
+}
diff --git a/xbmc/input/joysticks/generic/ButtonMapping.h b/xbmc/input/joysticks/generic/ButtonMapping.h
new file mode 100644
index 0000000..3f76767
--- /dev/null
+++ b/xbmc/input/joysticks/generic/ButtonMapping.h
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2014-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMapCallback.h"
+#include "input/joysticks/interfaces/IDriverHandler.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "input/mouse/MouseTypes.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+
+#include <chrono>
+#include <map>
+#include <memory>
+#include <stdint.h>
+
+class IKeymap;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CButtonMapping;
+class IButtonMap;
+class IButtonMapper;
+
+/*!
+ * \brief Detects and dispatches mapping events
+ *
+ * A mapping event usually occurs when a driver primitive is pressed or
+ * exceeds a certain threshold.
+ *
+ * Detection can be quite complicated due to driver bugs, so each type of
+ * driver primitive is given its own detector class inheriting from this one.
+ */
+class CPrimitiveDetector
+{
+protected:
+ CPrimitiveDetector(CButtonMapping* buttonMapping);
+
+ /*!
+ * \brief Dispatch a mapping event
+ *
+ * \return True if the primitive was mapped, false otherwise
+ */
+ bool MapPrimitive(const CDriverPrimitive& primitive);
+
+private:
+ CButtonMapping* const m_buttonMapping;
+};
+
+/*!
+ * \brief Detects when a button should be mapped
+ */
+class CButtonDetector : public CPrimitiveDetector
+{
+public:
+ CButtonDetector(CButtonMapping* buttonMapping, unsigned int buttonIndex);
+
+ /*!
+ * \brief Button state has been updated
+ *
+ * \param bPressed The new state
+ *
+ * \return True if this press was handled, false if it should fall through
+ * to the next driver handler
+ */
+ bool OnMotion(bool bPressed);
+
+private:
+ // Construction parameters
+ const unsigned int m_buttonIndex;
+};
+
+/*!
+ * \brief Detects when a D-pad direction should be mapped
+ */
+class CHatDetector : public CPrimitiveDetector
+{
+public:
+ CHatDetector(CButtonMapping* buttonMapping, unsigned int hatIndex);
+
+ /*!
+ * \brief Hat state has been updated
+ *
+ * \param state The new state
+ *
+ * \return True if state is a cardinal direction, false otherwise
+ */
+ bool OnMotion(HAT_STATE state);
+
+private:
+ // Construction parameters
+ const unsigned int m_hatIndex;
+};
+
+struct AxisConfiguration
+{
+ bool bKnown = false;
+ int center = 0;
+ unsigned int range = 1;
+ bool bLateDiscovery = false;
+};
+
+/*!
+ * \brief Detects when an axis should be mapped
+ */
+class CAxisDetector : public CPrimitiveDetector
+{
+public:
+ CAxisDetector(CButtonMapping* buttonMapping,
+ unsigned int axisIndex,
+ const AxisConfiguration& config);
+
+ /*!
+ * \brief Axis state has been updated
+ *
+ * \param position The new state
+ *
+ * \return Always true - axis motion events are always absorbed while button mapping
+ */
+ bool OnMotion(float position);
+
+ /*!
+ * \brief Called once per frame
+ *
+ * If an axis was activated, the button mapping command will be emitted
+ * here.
+ */
+ void ProcessMotion();
+
+ /*!
+ * \brief Check if the axis was mapped and is still in motion
+ *
+ * \return True between when the axis is mapped and when it crosses zero
+ */
+ bool IsMapping() const { return m_state == AXIS_STATE::MAPPED; }
+
+ /*!
+ * \brief Set the state such that this axis has generated a mapping event
+ *
+ * If an axis is mapped to the Select action, it may be pressed when button
+ * mapping begins. This function is used to indicate that the axis shouldn't
+ * be mapped until after it crosses zero again.
+ */
+ void SetEmitted(const CDriverPrimitive& activePrimitive);
+
+private:
+ enum class AXIS_STATE
+ {
+ /*!
+ * \brief Axis is inactive (position is less than threshold)
+ */
+ INACTIVE,
+
+ /*!
+ * \brief Axis is activated (position has exceeded threshold)
+ */
+ ACTIVATED,
+
+ /*!
+ * \brief Axis has generated a mapping event, but has not been centered yet
+ */
+ MAPPED,
+ };
+
+ enum class AXIS_TYPE
+ {
+ /*!
+ * \brief Axis type is initially unknown
+ */
+ UNKNOWN,
+
+ /*!
+ * \brief Axis is centered about 0
+ *
+ * - If the axis is an analog stick, it can travel to -1 or +1.
+ * - If the axis is a pressure-sensitive button or a normal trigger,
+ * it can travel to +1.
+ * - If the axis is a DirectInput trigger, then it is possible that two
+ * triggers can be on the same axis in opposite directions.
+ * - Normally, D-pads appear as a hat or four buttons. However, some
+ * D-pads are reported as two axes that can have the discrete values
+ * -1, 0 or 1. This is called a "discrete D-pad".
+ */
+ NORMAL,
+
+ /*!
+ * \brief Axis is centered about -1 or 1
+ *
+ * - On OSX, with the cocoa driver, triggers are centered about -1 and
+ * travel to +1. In this case, the range is 2 and the direction is
+ * positive.
+ * - The author of SDL has observed triggers centered at +1 and travel
+ * to 0. In this case, the range is 1 and the direction is negative.
+ */
+ OFFSET,
+ };
+
+ void DetectType(float position);
+
+ // Construction parameters
+ const unsigned int m_axisIndex;
+ AxisConfiguration m_config; // mutable
+
+ // State variables
+ AXIS_STATE m_state;
+ CDriverPrimitive m_activatedPrimitive;
+ AXIS_TYPE m_type;
+ bool m_initialPositionKnown; // set to true on first motion
+ float m_initialPosition; // set to position of first motion
+ bool m_initialPositionChanged; // set to true when position differs from the initial position
+ std::chrono::time_point<std::chrono::steady_clock>
+ m_activationTimeMs; // only used to delay anomalous trigger mapping to detect full range
+};
+
+/*!
+ * \brief Detects when a keyboard key should be mapped
+ */
+class CKeyDetector : public CPrimitiveDetector
+{
+public:
+ CKeyDetector(CButtonMapping* buttonMapping, XBMCKey keycode);
+
+ /*!
+ * \brief Key state has been updated
+ *
+ * \param bPressed The new state
+ *
+ * \return True if this press was handled, false if it should fall through
+ * to the next driver handler
+ */
+ bool OnMotion(bool bPressed);
+
+private:
+ // Construction parameters
+ const XBMCKey m_keycode;
+};
+
+/*!
+ * \brief Detects when a mouse button should be mapped
+ */
+class CMouseButtonDetector : public CPrimitiveDetector
+{
+public:
+ CMouseButtonDetector(CButtonMapping* buttonMapping, MOUSE::BUTTON_ID buttonIndex);
+
+ /*!
+ * \brief Button state has been updated
+ *
+ * \param bPressed The new state
+ *
+ * \return True if this press was handled, false if it should fall through
+ * to the next driver handler
+ */
+ bool OnMotion(bool bPressed);
+
+private:
+ // Construction parameters
+ const MOUSE::BUTTON_ID m_buttonIndex;
+};
+
+/*!
+ * \brief Detects when a mouse button should be mapped
+ */
+class CPointerDetector : public CPrimitiveDetector
+{
+public:
+ CPointerDetector(CButtonMapping* buttonMapping);
+
+ /*!
+ * \brief Pointer position has been updated
+ *
+ * \param x The new x coordinate
+ * \param y The new y coordinate
+ *
+ * \return Always true - pointer motion events are always absorbed while
+ * button mapping
+ */
+ bool OnMotion(int x, int y);
+
+private:
+ // Utility function
+ static INPUT::INTERCARDINAL_DIRECTION GetPointerDirection(int x, int y);
+
+ static const unsigned int MIN_FRAME_COUNT = 10;
+
+ // State variables
+ bool m_bStarted = false;
+ int m_startX = 0;
+ int m_startY = 0;
+ unsigned int m_frameCount = 0;
+};
+
+/*!
+ * \ingroup joystick
+ * \brief Generic implementation of a class that provides button mapping by
+ * translating driver events to button mapping commands
+ *
+ * Button mapping commands are invoked instantly for buttons and hats.
+ *
+ * Button mapping commands are deferred for a short while after an axis is
+ * activated, and only one button mapping command will be invoked per
+ * activation.
+ */
+class CButtonMapping : public IDriverHandler,
+ public KEYBOARD::IKeyboardDriverHandler,
+ public MOUSE::IMouseDriverHandler,
+ public IButtonMapCallback
+{
+public:
+ /*!
+ * \brief Constructor for CButtonMapping
+ *
+ * \param buttonMapper Carries out button-mapping commands using <buttonMap>
+ * \param buttonMap The button map given to <buttonMapper> on each command
+ */
+ CButtonMapping(IButtonMapper* buttonMapper, IButtonMap* buttonMap, IKeymap* keymap);
+
+ ~CButtonMapping() override = default;
+
+ // implementation of IDriverHandler
+ bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override;
+ bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) override;
+ bool OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range) override;
+ void OnInputFrame() override;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override {}
+
+ // implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(MOUSE::BUTTON_ID button) override;
+ void OnButtonRelease(MOUSE::BUTTON_ID button) override;
+
+ // implementation of IButtonMapCallback
+ void SaveButtonMap() override;
+ void ResetIgnoredPrimitives() override;
+ void RevertButtonMap() override;
+
+ /*!
+ * \brief Process the primitive mapping command
+ *
+ * First, this function checks if the input should be dropped. This can
+ * happen if the input is ignored or the cooldown period is active. If the
+ * input is dropped, this returns true with no effect, effectively absorbing
+ * the input. Otherwise, the mapping command is sent to m_buttonMapper.
+ *
+ * \param primitive The primitive being mapped
+ * \return True if the mapping command was handled, false otherwise
+ */
+ bool MapPrimitive(const CDriverPrimitive& primitive);
+
+private:
+ bool IsMapping() const;
+
+ void OnLateDiscovery(unsigned int axisIndex);
+
+ CButtonDetector& GetButton(unsigned int buttonIndex);
+ CHatDetector& GetHat(unsigned int hatIndex);
+ CAxisDetector& GetAxis(unsigned int axisIndex,
+ float position,
+ const AxisConfiguration& initialConfig = AxisConfiguration());
+ CKeyDetector& GetKey(XBMCKey keycode);
+ CMouseButtonDetector& GetMouseButton(MOUSE::BUTTON_ID buttonIndex);
+ CPointerDetector& GetPointer();
+
+ // Construction parameters
+ IButtonMapper* const m_buttonMapper;
+ IButtonMap* const m_buttonMap;
+ IKeymap* const m_keymap;
+
+ std::map<unsigned int, CButtonDetector> m_buttons;
+ std::map<unsigned int, CHatDetector> m_hats;
+ std::map<unsigned int, CAxisDetector> m_axes;
+ std::map<XBMCKey, CKeyDetector> m_keys;
+ std::map<MOUSE::BUTTON_ID, CMouseButtonDetector> m_mouseButtons;
+ std::unique_ptr<CPointerDetector> m_pointer;
+ std::chrono::time_point<std::chrono::steady_clock> m_lastAction;
+ uint64_t m_frameCount;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/generic/CMakeLists.txt b/xbmc/input/joysticks/generic/CMakeLists.txt
new file mode 100644
index 0000000..f44258a
--- /dev/null
+++ b/xbmc/input/joysticks/generic/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES ButtonMapping.cpp
+ DriverReceiving.cpp
+ FeatureHandling.cpp
+ InputHandling.cpp)
+
+set(HEADERS ButtonMapping.h
+ DriverReceiving.h
+ FeatureHandling.h
+ InputHandling.h)
+
+core_add_library(input_joystick_generic)
diff --git a/xbmc/input/joysticks/generic/DriverReceiving.cpp b/xbmc/input/joysticks/generic/DriverReceiving.cpp
new file mode 100644
index 0000000..bf52f4f
--- /dev/null
+++ b/xbmc/input/joysticks/generic/DriverReceiving.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "DriverReceiving.h"
+
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IDriverReceiver.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CDriverReceiving::CDriverReceiving(IDriverReceiver* receiver, IButtonMap* buttonMap)
+ : m_receiver(receiver), m_buttonMap(buttonMap)
+{
+}
+
+bool CDriverReceiving::SetRumbleState(const FeatureName& feature, float magnitude)
+{
+ bool bHandled = false;
+
+ if (m_receiver != nullptr && m_buttonMap != nullptr)
+ {
+ CDriverPrimitive primitive;
+ if (m_buttonMap->GetScalar(feature, primitive))
+ {
+ if (primitive.Type() == PRIMITIVE_TYPE::MOTOR)
+ bHandled = m_receiver->SetMotorState(primitive.Index(), magnitude);
+ }
+ }
+
+ return bHandled;
+}
diff --git a/xbmc/input/joysticks/generic/DriverReceiving.h b/xbmc/input/joysticks/generic/DriverReceiving.h
new file mode 100644
index 0000000..758b560
--- /dev/null
+++ b/xbmc/input/joysticks/generic/DriverReceiving.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IInputReceiver.h"
+
+#include <map>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IDriverReceiver;
+class IButtonMap;
+
+/*!
+ * \ingroup joystick
+ * \brief Class to translate input events from higher-level features to driver primitives
+ *
+ * A button map is used to translate controller features to driver primitives.
+ * The button map has been abstracted away behind the IButtonMap interface
+ * so that it can be provided by an add-on.
+ */
+class CDriverReceiving : public IInputReceiver
+{
+public:
+ CDriverReceiving(IDriverReceiver* receiver, IButtonMap* buttonMap);
+
+ ~CDriverReceiving() override = default;
+
+ // implementation of IInputReceiver
+ bool SetRumbleState(const FeatureName& feature, float magnitude) override;
+
+private:
+ IDriverReceiver* const m_receiver;
+ IButtonMap* const m_buttonMap;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/generic/FeatureHandling.cpp b/xbmc/input/joysticks/generic/FeatureHandling.cpp
new file mode 100644
index 0000000..639ae5b
--- /dev/null
+++ b/xbmc/input/joysticks/generic/FeatureHandling.cpp
@@ -0,0 +1,551 @@
+/*
+ * Copyright (C) 2014-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "FeatureHandling.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "utils/log.h"
+
+#include <vector>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+#define ANALOG_DIGITAL_THRESHOLD 0.5f
+#define DISCRETE_ANALOG_RAMPUP_TIME_MS 1500
+#define DISCRETE_ANALOG_START_VALUE 0.3f
+
+// --- CJoystickFeature --------------------------------------------------------
+
+CJoystickFeature::CJoystickFeature(const FeatureName& name,
+ IInputHandler* handler,
+ IButtonMap* buttonMap)
+ : m_name(name),
+ m_handler(handler),
+ m_buttonMap(buttonMap),
+ m_bEnabled(m_handler->HasFeature(name))
+{
+}
+
+bool CJoystickFeature::AcceptsInput(bool bActivation)
+{
+ bool bAcceptsInput = false;
+
+ if (m_bEnabled)
+ {
+ if (m_handler->AcceptsInput(m_name))
+ bAcceptsInput = true;
+ }
+
+ return bAcceptsInput;
+}
+
+void CJoystickFeature::ResetMotion()
+{
+ m_motionStartTimeMs = {};
+}
+
+void CJoystickFeature::StartMotion()
+{
+ m_motionStartTimeMs = std::chrono::steady_clock::now();
+}
+
+bool CJoystickFeature::InMotion() const
+{
+ return m_motionStartTimeMs.time_since_epoch().count() > 0;
+}
+
+unsigned int CJoystickFeature::MotionTimeMs() const
+{
+ if (!InMotion())
+ return 0;
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_motionStartTimeMs);
+
+ return duration.count();
+}
+
+// --- CScalarFeature ----------------------------------------------------------
+
+CScalarFeature::CScalarFeature(const FeatureName& name,
+ IInputHandler* handler,
+ IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap),
+ m_bDigitalState(false),
+ m_analogState(0.0f),
+ m_bActivated(false),
+ m_bDiscrete(true)
+{
+ GAME::ControllerPtr controller =
+ CServiceBroker::GetGameControllerManager().GetController(handler->ControllerID());
+ if (controller)
+ m_inputType = controller->GetInputType(name);
+}
+
+bool CScalarFeature::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(bPressed);
+
+ if (m_inputType == INPUT_TYPE::DIGITAL)
+ bHandled &= OnDigitalMotion(bPressed);
+ else if (m_inputType == INPUT_TYPE::ANALOG)
+ bHandled &= OnAnalogMotion(bPressed ? 1.0f : 0.0f);
+
+ return bHandled;
+}
+
+bool CScalarFeature::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ // Update activated status
+ if (magnitude > 0.0f)
+ m_bActivated = true;
+
+ // Update discrete status
+ if (magnitude != 0.0f && magnitude != 1.0f)
+ m_bDiscrete = false;
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ if (m_inputType == INPUT_TYPE::DIGITAL)
+ bHandled &= OnDigitalMotion(magnitude >= ANALOG_DIGITAL_THRESHOLD);
+ else if (m_inputType == INPUT_TYPE::ANALOG)
+ bHandled &= OnAnalogMotion(magnitude);
+
+ return bHandled;
+}
+
+void CScalarFeature::ProcessMotions(void)
+{
+ if (m_inputType == INPUT_TYPE::DIGITAL && m_bDigitalState)
+ ProcessDigitalMotion();
+ else if (m_inputType == INPUT_TYPE::ANALOG)
+ ProcessAnalogMotion();
+}
+
+bool CScalarFeature::OnDigitalMotion(bool bPressed)
+{
+ bool bHandled = false;
+
+ if (m_bDigitalState != bPressed)
+ {
+ m_bDigitalState = bPressed;
+
+ // Motion is initiated in ProcessMotions()
+ ResetMotion();
+
+ bHandled = m_bInitialPressHandled = m_handler->OnButtonPress(m_name, bPressed);
+
+ if (m_bDigitalState)
+ CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} pressed ({})", m_name, m_handler->ControllerID(),
+ bHandled ? "handled" : "ignored");
+ else
+ CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} released", m_name, m_handler->ControllerID());
+ }
+ else if (m_bDigitalState)
+ {
+ bHandled = m_bInitialPressHandled;
+ }
+
+ return bHandled;
+}
+
+bool CScalarFeature::OnAnalogMotion(float magnitude)
+{
+ const bool bActivated = (magnitude != 0.0f);
+
+ // Update analog state
+ m_analogState = magnitude;
+
+ // Update motion time
+ if (!bActivated)
+ ResetMotion();
+ else if (!InMotion())
+ StartMotion();
+
+ // Log activation/deactivation
+ if (m_bDigitalState != bActivated)
+ {
+ m_bDigitalState = bActivated;
+ CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} {}", m_name, m_handler->ControllerID(),
+ bActivated ? "activated" : "deactivated");
+ }
+
+ return true;
+}
+
+void CScalarFeature::ProcessDigitalMotion()
+{
+ if (!InMotion())
+ {
+ // Button was just pressed, record start time and exit (button press event
+ // was already sent this frame)
+ StartMotion();
+ }
+ else
+ {
+ // Button has been pressed more than one event frame
+ const unsigned int elapsed = MotionTimeMs();
+ m_handler->OnButtonHold(m_name, elapsed);
+ }
+}
+
+void CScalarFeature::ProcessAnalogMotion()
+{
+ float magnitude = m_analogState;
+
+ // Calculate time elapsed since motion began
+ unsigned int elapsed = MotionTimeMs();
+
+ // If analog value is discrete, ramp up magnitude
+ if (m_bActivated && m_bDiscrete)
+ {
+ if (elapsed < DISCRETE_ANALOG_RAMPUP_TIME_MS)
+ {
+ magnitude *= static_cast<float>(elapsed) / DISCRETE_ANALOG_RAMPUP_TIME_MS;
+ if (magnitude < DISCRETE_ANALOG_START_VALUE)
+ magnitude = DISCRETE_ANALOG_START_VALUE;
+ }
+ }
+
+ m_handler->OnButtonMotion(m_name, magnitude, elapsed);
+}
+
+// --- CAxisFeature ------------------------------------------------------------
+
+CAxisFeature::CAxisFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap), m_state(0.0f)
+{
+}
+
+bool CAxisFeature::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f);
+}
+
+void CAxisFeature::ProcessMotions(void)
+{
+ const float newState = m_axis.GetPosition();
+
+ const bool bActivated = (newState != 0.0f);
+
+ if (!AcceptsInput(bActivated))
+ return;
+
+ const bool bWasActivated = (m_state != 0.0f);
+
+ if (!bActivated && bWasActivated)
+ CLog::Log(LOGDEBUG, "Feature [ {} ] on {} deactivated", m_name, m_handler->ControllerID());
+ else if (bActivated && !bWasActivated)
+ {
+ CLog::Log(LOGDEBUG, "Feature [ {} ] on {} activated {}", m_name, m_handler->ControllerID(),
+ newState > 0.0f ? "positive" : "negative");
+ }
+
+ if (bActivated || bWasActivated)
+ {
+ m_state = newState;
+
+ unsigned int motionTimeMs = 0;
+
+ if (bActivated)
+ {
+ if (!InMotion())
+ StartMotion();
+ else
+ motionTimeMs = MotionTimeMs();
+ }
+ else
+ ResetMotion();
+
+ switch (m_buttonMap->GetFeatureType(m_name))
+ {
+ case FEATURE_TYPE::WHEEL:
+ m_handler->OnWheelMotion(m_name, newState, motionTimeMs);
+ break;
+ case FEATURE_TYPE::THROTTLE:
+ m_handler->OnThrottleMotion(m_name, newState, motionTimeMs);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+// --- CWheel ------------------------------------------------------------------
+
+CWheel::CWheel(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CAxisFeature(name, handler, buttonMap)
+{
+}
+
+bool CWheel::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ WHEEL_DIRECTION direction = WHEEL_DIRECTION::NONE;
+
+ std::vector<WHEEL_DIRECTION> dirs = {
+ WHEEL_DIRECTION::RIGHT,
+ WHEEL_DIRECTION::LEFT,
+ };
+
+ CDriverPrimitive primitive;
+ for (auto dir : dirs)
+ {
+ if (m_buttonMap->GetWheel(m_name, dir, primitive) && primitive == source)
+ {
+ direction = dir;
+ break;
+ }
+ }
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ switch (direction)
+ {
+ case WHEEL_DIRECTION::RIGHT:
+ m_axis.SetPositiveDistance(magnitude);
+ break;
+ case WHEEL_DIRECTION::LEFT:
+ m_axis.SetNegativeDistance(magnitude);
+ break;
+ default:
+ // Just in case, avoid sticking
+ m_axis.Reset();
+ break;
+ }
+
+ return bHandled;
+}
+
+// --- CThrottle ---------------------------------------------------------------
+
+CThrottle::CThrottle(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CAxisFeature(name, handler, buttonMap)
+{
+}
+
+bool CThrottle::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ THROTTLE_DIRECTION direction = THROTTLE_DIRECTION::NONE;
+
+ std::vector<THROTTLE_DIRECTION> dirs = {
+ THROTTLE_DIRECTION::UP,
+ THROTTLE_DIRECTION::DOWN,
+ };
+
+ CDriverPrimitive primitive;
+ for (auto dir : dirs)
+ {
+ if (m_buttonMap->GetThrottle(m_name, dir, primitive) && primitive == source)
+ {
+ direction = dir;
+ break;
+ }
+ }
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ switch (direction)
+ {
+ case THROTTLE_DIRECTION::UP:
+ m_axis.SetPositiveDistance(magnitude);
+ break;
+ case THROTTLE_DIRECTION::DOWN:
+ m_axis.SetNegativeDistance(magnitude);
+ break;
+ default:
+ // Just in case, avoid sticking
+ m_axis.Reset();
+ break;
+ }
+
+ return bHandled;
+}
+
+// --- CAnalogStick ------------------------------------------------------------
+
+CAnalogStick::CAnalogStick(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap), m_vertState(0.0f), m_horizState(0.0f)
+{
+}
+
+bool CAnalogStick::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f);
+}
+
+bool CAnalogStick::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ ANALOG_STICK_DIRECTION direction = ANALOG_STICK_DIRECTION::NONE;
+
+ std::vector<ANALOG_STICK_DIRECTION> dirs = {
+ ANALOG_STICK_DIRECTION::UP,
+ ANALOG_STICK_DIRECTION::DOWN,
+ ANALOG_STICK_DIRECTION::RIGHT,
+ ANALOG_STICK_DIRECTION::LEFT,
+ };
+
+ CDriverPrimitive primitive;
+ for (auto dir : dirs)
+ {
+ if (m_buttonMap->GetAnalogStick(m_name, dir, primitive) && primitive == source)
+ {
+ direction = dir;
+ break;
+ }
+ }
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ switch (direction)
+ {
+ case ANALOG_STICK_DIRECTION::UP:
+ m_vertAxis.SetPositiveDistance(magnitude);
+ break;
+ case ANALOG_STICK_DIRECTION::DOWN:
+ m_vertAxis.SetNegativeDistance(magnitude);
+ break;
+ case ANALOG_STICK_DIRECTION::RIGHT:
+ m_horizAxis.SetPositiveDistance(magnitude);
+ break;
+ case ANALOG_STICK_DIRECTION::LEFT:
+ m_horizAxis.SetNegativeDistance(magnitude);
+ break;
+ default:
+ // Just in case, avoid sticking
+ m_vertAxis.Reset();
+ m_horizAxis.Reset();
+ break;
+ }
+
+ return bHandled;
+}
+
+void CAnalogStick::ProcessMotions(void)
+{
+ const float newVertState = m_vertAxis.GetPosition();
+ const float newHorizState = m_horizAxis.GetPosition();
+
+ const bool bActivated = (newVertState != 0.0f || newHorizState != 0.0f);
+
+ if (!AcceptsInput(bActivated))
+ return;
+
+ const bool bWasActivated = (m_vertState != 0.0f || m_horizState != 0.0f);
+
+ if (bActivated ^ bWasActivated)
+ {
+ CLog::Log(LOGDEBUG, "Feature [ {} ] on {} {}", m_name, m_handler->ControllerID(),
+ bActivated ? "activated" : "deactivated");
+ }
+
+ if (bActivated || bWasActivated)
+ {
+ m_vertState = newVertState;
+ m_horizState = newHorizState;
+
+ unsigned int motionTimeMs = 0;
+
+ if (bActivated)
+ {
+ if (!InMotion())
+ StartMotion();
+ else
+ motionTimeMs = MotionTimeMs();
+ }
+ else
+ {
+ ResetMotion();
+ }
+
+ m_handler->OnAnalogStickMotion(m_name, newHorizState, newVertState, motionTimeMs);
+ }
+}
+
+// --- CAccelerometer ----------------------------------------------------------
+
+CAccelerometer::CAccelerometer(const FeatureName& name,
+ IInputHandler* handler,
+ IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap),
+ m_xAxisState(0.0f),
+ m_yAxisState(0.0f),
+ m_zAxisState(0.0f)
+{
+}
+
+bool CAccelerometer::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f);
+}
+
+bool CAccelerometer::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(true);
+
+ CDriverPrimitive positiveX;
+ CDriverPrimitive positiveY;
+ CDriverPrimitive positiveZ;
+
+ m_buttonMap->GetAccelerometer(m_name, positiveX, positiveY, positiveZ);
+
+ if (source == positiveX)
+ {
+ m_xAxis.SetPositiveDistance(magnitude);
+ }
+ else if (source == positiveY)
+ {
+ m_yAxis.SetPositiveDistance(magnitude);
+ }
+ else if (source == positiveZ)
+ {
+ m_zAxis.SetPositiveDistance(magnitude);
+ }
+ else
+ {
+ // Just in case, avoid sticking
+ m_xAxis.Reset();
+ m_xAxis.Reset();
+ m_yAxis.Reset();
+ }
+
+ return bHandled;
+}
+
+void CAccelerometer::ProcessMotions(void)
+{
+ const float newXAxis = m_xAxis.GetPosition();
+ const float newYAxis = m_yAxis.GetPosition();
+ const float newZAxis = m_zAxis.GetPosition();
+
+ const bool bActivated = (newXAxis != 0.0f || newYAxis != 0.0f || newZAxis != 0.0f);
+
+ if (!AcceptsInput(bActivated))
+ return;
+
+ const bool bWasActivated = (m_xAxisState != 0.0f || m_yAxisState != 0.0f || m_zAxisState != 0.0f);
+
+ if (bActivated || bWasActivated)
+ {
+ m_xAxisState = newXAxis;
+ m_yAxisState = newYAxis;
+ m_zAxisState = newZAxis;
+ m_handler->OnAccelerometerMotion(m_name, newXAxis, newYAxis, newZAxis);
+ }
+}
diff --git a/xbmc/input/joysticks/generic/FeatureHandling.h b/xbmc/input/joysticks/generic/FeatureHandling.h
new file mode 100644
index 0000000..1a42d5c
--- /dev/null
+++ b/xbmc/input/joysticks/generic/FeatureHandling.h
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "input/joysticks/JoystickTypes.h"
+
+#include <chrono>
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CDriverPrimitive;
+class IInputHandler;
+class IButtonMap;
+
+class CJoystickFeature;
+using FeaturePtr = std::shared_ptr<CJoystickFeature>;
+
+/*!
+ * \ingroup joystick
+ * \brief Base class for joystick features
+ *
+ * See list of feature types in JoystickTypes.h.
+ */
+class CJoystickFeature
+{
+public:
+ CJoystickFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ virtual ~CJoystickFeature() = default;
+
+ /*!
+ * \brief A digital motion has occurred
+ *
+ * \param source The source of the motion. Must be digital (button or hat)
+ * \param bPressed True for press motion, false for release motion
+ *
+ * \return true if the motion was handled, false otherwise
+ */
+ virtual bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) = 0;
+
+ /*!
+ * \brief An analog motion has occurred
+ *
+ * \param source The source of the motion. Must be a semiaxis
+ * \param magnitude The magnitude of the press or motion in the interval [0.0, 1.0]
+ *
+ * For semiaxes, the magnitude is the force or travel distance in the
+ * direction of the semiaxis. If the value is in the opposite direction,
+ * the magnitude is 0.0.
+ *
+ * For example, if the analog stick goes left, the negative semiaxis will
+ * have a value of 1.0 and the positive semiaxis will have a value of 0.0.
+ */
+ virtual bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) = 0;
+
+ /*!
+ * \brief Process the motions that have occurred since the last invocation
+ *
+ * This allows features with motion on multiple driver primitives to call
+ * their handler once all driver primitives are accounted for.
+ */
+ virtual void ProcessMotions(void) = 0;
+
+ /*!
+ * \brief Check if the input handler is accepting input
+ *
+ * \param bActivation True if the motion is activating (true or positive),
+ * false if the motion is deactivating (false or zero)
+ *
+ * \return True if input should be sent to the input handler, false otherwise
+ */
+ bool AcceptsInput(bool bActivation);
+
+protected:
+ /*!
+ * \brief Reset motion timer
+ */
+ void ResetMotion();
+
+ /*!
+ * \brief Start the motion timer
+ */
+ void StartMotion();
+
+ /*!
+ * \brief Check if the feature is in motion
+ */
+ bool InMotion() const;
+
+ /*!
+ * \brief Get the time for which the feature has been in motion
+ */
+ unsigned int MotionTimeMs() const;
+
+ const FeatureName m_name;
+ IInputHandler* const m_handler;
+ IButtonMap* const m_buttonMap;
+ const bool m_bEnabled;
+
+private:
+ std::chrono::time_point<std::chrono::steady_clock> m_motionStartTimeMs;
+};
+
+class CScalarFeature : public CJoystickFeature
+{
+public:
+ CScalarFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CScalarFeature() override = default;
+
+ // implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+ void ProcessMotions() override;
+
+private:
+ bool OnDigitalMotion(bool bPressed);
+ bool OnAnalogMotion(float magnitude);
+
+ void ProcessDigitalMotion();
+ void ProcessAnalogMotion();
+
+ // State variables
+ INPUT_TYPE m_inputType = INPUT_TYPE::UNKNOWN;
+ bool m_bDigitalState;
+ bool m_bInitialPressHandled = false;
+
+ // Analog state variables
+ float m_analogState; // The current magnitude
+ float m_bActivated; // Set to true when first activated (magnitude > 0.0)
+ bool m_bDiscrete; // Set to false when a non-discrete axis is detected
+};
+
+/*!
+ * \ingroup joystick
+ * \brief Axis of a feature (analog stick, accelerometer, etc)
+ *
+ * Axes are composed of two driver primitives, one for the positive semiaxis
+ * and one for the negative semiaxis.
+ *
+ * This effectively means that an axis is two-dimensional, with each dimension
+ * either:
+ *
+ * - a digital value (0.0 or 1.0)
+ * - an analog value (continuous in the interval [0.0, 1.0])
+ */
+class CFeatureAxis
+{
+public:
+ CFeatureAxis(void) { Reset(); }
+
+ /*!
+ * \brief Set value of positive axis
+ */
+ void SetPositiveDistance(float distance) { m_positiveDistance = distance; }
+
+ /*!
+ * \brief Set value of negative axis
+ */
+ void SetNegativeDistance(float distance) { m_negativeDistance = distance; }
+
+ /*!
+ * \brief Get the final value of this axis.
+ *
+ * This axis is two-dimensional, so we need to compress these into a single
+ * dimension. This is done by subtracting the negative from the positive.
+ * Some examples:
+ *
+ * Positive axis: 1.0 (User presses right or analog stick moves right)
+ * Negative axis: 0.0
+ * -------------------
+ * Pos - Neg: 1.0 (Emulated analog stick moves right)
+ *
+ *
+ * Positive axis: 0.0
+ * Negative axis: 1.0 (User presses left or analog stick moves left)
+ * -------------------
+ * Pos - Neg: -1.0 (Emulated analog stick moves left)
+ *
+ *
+ * Positive axis: 1.0 (User presses both buttons)
+ * Negative axis: 1.0
+ * -------------------
+ * Pos - Neg: 0.0 (Emulated analog stick is centered)
+ *
+ */
+ float GetPosition(void) const { return m_positiveDistance - m_negativeDistance; }
+
+ /*!
+ * \brief Reset both positive and negative values to zero
+ */
+ void Reset(void) { m_positiveDistance = m_negativeDistance = 0.0f; }
+
+protected:
+ float m_positiveDistance;
+ float m_negativeDistance;
+};
+
+class CAxisFeature : public CJoystickFeature
+{
+public:
+ CAxisFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CAxisFeature() override = default;
+
+ // partial implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ void ProcessMotions() override;
+
+protected:
+ CFeatureAxis m_axis;
+
+ float m_state;
+};
+
+class CWheel : public CAxisFeature
+{
+public:
+ CWheel(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CWheel() override = default;
+
+ // partial implementation of CJoystickFeature
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+};
+
+class CThrottle : public CAxisFeature
+{
+public:
+ CThrottle(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CThrottle() override = default;
+
+ // partial implementation of CJoystickFeature
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+};
+
+class CAnalogStick : public CJoystickFeature
+{
+public:
+ CAnalogStick(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CAnalogStick() override = default;
+
+ // implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+ void ProcessMotions() override;
+
+protected:
+ CFeatureAxis m_vertAxis;
+ CFeatureAxis m_horizAxis;
+
+ float m_vertState;
+ float m_horizState;
+};
+
+class CAccelerometer : public CJoystickFeature
+{
+public:
+ CAccelerometer(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CAccelerometer() override = default;
+
+ // implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+ void ProcessMotions() override;
+
+protected:
+ CFeatureAxis m_xAxis;
+ CFeatureAxis m_yAxis;
+ CFeatureAxis m_zAxis;
+
+ float m_xAxisState;
+ float m_yAxisState;
+ float m_zAxisState;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/generic/InputHandling.cpp b/xbmc/input/joysticks/generic/InputHandling.cpp
new file mode 100644
index 0000000..f6cae6d
--- /dev/null
+++ b/xbmc/input/joysticks/generic/InputHandling.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2014-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "InputHandling.h"
+
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/dialogs/GUIDialogNewJoystick.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "utils/log.h"
+
+#include <array>
+#include <cmath>
+#include <tuple>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CGUIDialogNewJoystick* const CInputHandling::m_dialog = new CGUIDialogNewJoystick;
+
+CInputHandling::CInputHandling(IInputHandler* handler, IButtonMap* buttonMap)
+ : m_handler(handler), m_buttonMap(buttonMap)
+{
+}
+
+CInputHandling::~CInputHandling(void) = default;
+
+bool CInputHandling::OnButtonMotion(unsigned int buttonIndex, bool bPressed)
+{
+ return OnDigitalMotion(CDriverPrimitive(PRIMITIVE_TYPE::BUTTON, buttonIndex), bPressed);
+}
+
+bool CInputHandling::OnHatMotion(unsigned int hatIndex, HAT_STATE state)
+{
+ bool bHandled = false;
+
+ bHandled |=
+ OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::UP), state & HAT_DIRECTION::UP);
+ bHandled |= OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::RIGHT),
+ state & HAT_DIRECTION::RIGHT);
+ bHandled |=
+ OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::DOWN), state & HAT_DIRECTION::DOWN);
+ bHandled |=
+ OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::LEFT), state & HAT_DIRECTION::LEFT);
+
+ return bHandled;
+}
+
+bool CInputHandling::OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range)
+{
+ bool bHandled = false;
+
+ if (center != 0)
+ {
+ float translatedPostion = std::min((position - center) / range, 1.0f);
+
+ // Calculate the direction the trigger travels from the center point
+ SEMIAXIS_DIRECTION dir;
+ if (center > 0)
+ dir = SEMIAXIS_DIRECTION::NEGATIVE;
+ else
+ dir = SEMIAXIS_DIRECTION::POSITIVE;
+
+ CDriverPrimitive offsetSemiaxis(axisIndex, center, dir, range);
+
+ bHandled = OnAnalogMotion(offsetSemiaxis, translatedPostion);
+ }
+ else
+ {
+ CDriverPrimitive positiveSemiaxis(axisIndex, 0, SEMIAXIS_DIRECTION::POSITIVE, 1);
+ CDriverPrimitive negativeSemiaxis(axisIndex, 0, SEMIAXIS_DIRECTION::NEGATIVE, 1);
+
+ bHandled |= OnAnalogMotion(positiveSemiaxis, position > 0.0f ? position : 0.0f);
+ bHandled |= OnAnalogMotion(negativeSemiaxis, position < 0.0f ? -position : 0.0f);
+ }
+
+ return bHandled;
+}
+
+void CInputHandling::OnInputFrame(void)
+{
+ // Handle driver input
+ for (auto& it : m_features)
+ it.second->ProcessMotions();
+
+ // Handle higher-level controller input
+ m_handler->OnInputFrame();
+}
+
+bool CInputHandling::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ bool bHandled = false;
+
+ FeatureName featureName;
+ if (m_buttonMap->GetFeature(source, featureName))
+ {
+ auto it = m_features.find(featureName);
+ if (it == m_features.end())
+ {
+ FeaturePtr feature(CreateFeature(featureName));
+ if (feature)
+ std::tie(it, std::ignore) = m_features.insert({featureName, std::move(feature)});
+ }
+
+ if (it != m_features.end())
+ bHandled = it->second->OnDigitalMotion(source, bPressed);
+ }
+ else if (bPressed)
+ {
+ // If button didn't resolve to a feature, check if the button map is empty
+ // and ask the user if they would like to start mapping the controller
+ if (m_buttonMap->IsEmpty())
+ {
+ CLog::Log(LOGDEBUG, "Empty button map detected for {}", m_buttonMap->ControllerID());
+ m_dialog->ShowAsync();
+ }
+ }
+
+ return bHandled;
+}
+
+bool CInputHandling::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ bool bHandled = false;
+
+ FeatureName featureName;
+ if (m_buttonMap->GetFeature(source, featureName))
+ {
+ auto it = m_features.find(featureName);
+ if (it == m_features.end())
+ {
+ FeaturePtr feature(CreateFeature(featureName));
+ if (feature)
+ std::tie(it, std::ignore) = m_features.insert({featureName, std::move(feature)});
+ }
+
+ if (it != m_features.end())
+ bHandled = it->second->OnAnalogMotion(source, magnitude);
+ }
+
+ return bHandled;
+}
+
+CJoystickFeature* CInputHandling::CreateFeature(const FeatureName& featureName)
+{
+ CJoystickFeature* feature = nullptr;
+
+ switch (m_buttonMap->GetFeatureType(featureName))
+ {
+ case FEATURE_TYPE::SCALAR:
+ {
+ feature = new CScalarFeature(featureName, m_handler, m_buttonMap);
+ break;
+ }
+ case FEATURE_TYPE::ANALOG_STICK:
+ {
+ feature = new CAnalogStick(featureName, m_handler, m_buttonMap);
+ break;
+ }
+ case FEATURE_TYPE::ACCELEROMETER:
+ {
+ feature = new CAccelerometer(featureName, m_handler, m_buttonMap);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return feature;
+}
diff --git a/xbmc/input/joysticks/generic/InputHandling.h b/xbmc/input/joysticks/generic/InputHandling.h
new file mode 100644
index 0000000..772d9d1
--- /dev/null
+++ b/xbmc/input/joysticks/generic/InputHandling.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "FeatureHandling.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IDriverHandler.h"
+
+#include <map>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CDriverPrimitive;
+class CGUIDialogNewJoystick;
+class IInputHandler;
+class IButtonMap;
+
+/*!
+ * \ingroup joystick
+ * \brief Class to translate input from the driver into higher-level features
+ *
+ * Raw driver input arrives for three elements: buttons, hats and axes. When
+ * driver input is handled by this class, it translates the raw driver
+ * elements into physical joystick features, such as buttons, analog sticks,
+ * etc.
+ *
+ * A button map is used to translate driver primitives to controller features.
+ * The button map has been abstracted away behind the IButtonMap
+ * interface so that it can be provided by an add-on.
+ */
+class CInputHandling : public IDriverHandler
+{
+public:
+ CInputHandling(IInputHandler* handler, IButtonMap* buttonMap);
+
+ ~CInputHandling() override;
+
+ // implementation of IDriverHandler
+ bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override;
+ bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) override;
+ bool OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range) override;
+ void OnInputFrame() override;
+
+private:
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed);
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude);
+
+ CJoystickFeature* CreateFeature(const FeatureName& featureName);
+
+ IInputHandler* const m_handler;
+ IButtonMap* const m_buttonMap;
+
+ std::map<FeatureName, FeaturePtr> m_features;
+
+ static CGUIDialogNewJoystick* const m_dialog;
+};
+} // namespace JOYSTICK
+} // namespace KODI