summaryrefslogtreecommitdiffstats
path: root/xbmc/games/addons/input/GameClientInput.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/games/addons/input/GameClientInput.cpp')
-rw-r--r--xbmc/games/addons/input/GameClientInput.cpp697
1 files changed, 697 insertions, 0 deletions
diff --git a/xbmc/games/addons/input/GameClientInput.cpp b/xbmc/games/addons/input/GameClientInput.cpp
new file mode 100644
index 0000000..5805cbf
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientInput.cpp
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2017-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 "GameClientInput.h"
+
+#include "GameClientController.h"
+#include "GameClientHardware.h"
+#include "GameClientJoystick.h"
+#include "GameClientKeyboard.h"
+#include "GameClientMouse.h"
+#include "GameClientPort.h"
+#include "GameClientTopology.h"
+#include "ServiceBroker.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/GameServices.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/GameClientCallbacks.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/controllers/types/ControllerNode.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "games/ports/input/PortManager.h"
+#include "games/ports/types/PortNode.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "peripherals/EventLockHandle.h"
+#include "peripherals/Peripherals.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientInput::CGameClientInput(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess)
+ : CGameClientSubsystem(gameClient, addonStruct, clientAccess),
+ m_topology(new CGameClientTopology),
+ m_portManager(std::make_unique<CPortManager>())
+{
+}
+
+CGameClientInput::~CGameClientInput()
+{
+ Deinitialize();
+}
+
+void CGameClientInput::Initialize()
+{
+ LoadTopology();
+
+ // Send controller layouts to game client
+ SetControllerLayouts(m_topology->GetControllerTree().GetControllers());
+
+ // Reset ports to default state (first accepted controller is connected)
+ ActivateControllers(m_topology->GetControllerTree());
+
+ // Initialize the port manager
+ m_portManager->Initialize(m_gameClient.Profile());
+ m_portManager->SetControllerTree(m_topology->GetControllerTree());
+ m_portManager->LoadXML();
+}
+
+void CGameClientInput::Start(IGameInputCallback* input)
+{
+ m_inputCallback = input;
+
+ // Connect/disconnect active controllers
+ for (const CPortNode& port : GetActiveControllerTree().GetPorts())
+ {
+ if (port.IsConnected())
+ {
+ const ControllerPtr& activeController = port.GetActiveController().GetController();
+ if (activeController)
+ ConnectController(port.GetAddress(), activeController);
+ }
+ else
+ DisconnectController(port.GetAddress());
+ }
+
+ // Ensure hardware is open to receive events
+ m_hardware.reset(new CGameClientHardware(m_gameClient));
+
+ // Notify observers of the initial port configuration
+ NotifyObservers(ObservableMessageGamePortsChanged);
+}
+
+void CGameClientInput::Deinitialize()
+{
+ Stop();
+
+ m_topology->Clear();
+ m_controllerLayouts.clear();
+ m_portManager->Clear();
+}
+
+void CGameClientInput::Stop()
+{
+ m_hardware.reset();
+
+ CloseMouse();
+
+ CloseKeyboard();
+
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+ CloseJoysticks(inputHandlingLock);
+
+ // If a port was closed, then this blocks until all peripheral input has
+ // been handled
+ inputHandlingLock.reset();
+
+ m_inputCallback = nullptr;
+}
+
+bool CGameClientInput::HasFeature(const std::string& controllerId,
+ const std::string& featureName) const
+{
+ bool bHasFeature = false;
+
+ try
+ {
+ bHasFeature =
+ m_struct.toAddon->HasFeature(&m_struct, controllerId.c_str(), featureName.c_str());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: exception caught in HasFeature()", m_gameClient.ID());
+
+ // Fail gracefully
+ bHasFeature = true;
+ }
+
+ return bHasFeature;
+}
+
+bool CGameClientInput::AcceptsInput() const
+{
+ if (m_inputCallback != nullptr)
+ return m_inputCallback->AcceptsInput();
+
+ return false;
+}
+
+bool CGameClientInput::InputEvent(const game_input_event& event)
+{
+ bool bHandled = false;
+
+ try
+ {
+ bHandled = m_struct.toAddon->InputEvent(&m_struct, &event);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: exception caught in InputEvent()", m_gameClient.ID());
+ }
+
+ return bHandled;
+}
+
+void CGameClientInput::LoadTopology()
+{
+ game_input_topology* topologyStruct = nullptr;
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ topologyStruct = m_struct.toAddon->GetTopology(&m_struct);
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("GetTopology()");
+ }
+ }
+
+ GameClientPortVec hardwarePorts;
+ int playerLimit = -1;
+
+ if (topologyStruct != nullptr)
+ {
+ //! @todo Guard against infinite loops provided by the game client
+
+ game_input_port* ports = topologyStruct->ports;
+ if (ports != nullptr)
+ {
+ for (unsigned int i = 0; i < topologyStruct->port_count; i++)
+ hardwarePorts.emplace_back(new CGameClientPort(ports[i]));
+ }
+
+ playerLimit = topologyStruct->player_limit;
+
+ try
+ {
+ m_struct.toAddon->FreeTopology(&m_struct, topologyStruct);
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("FreeTopology()");
+ }
+ }
+
+ // If no topology is available, create a default one with a single port that
+ // accepts all controllers imported by addon.xml
+ if (hardwarePorts.empty())
+ hardwarePorts.emplace_back(new CGameClientPort(GetControllers(m_gameClient)));
+
+ m_topology.reset(new CGameClientTopology(std::move(hardwarePorts), playerLimit));
+}
+
+void CGameClientInput::ActivateControllers(CControllerHub& hub)
+{
+ for (auto& port : hub.GetPorts())
+ {
+ if (port.GetCompatibleControllers().empty())
+ continue;
+
+ port.SetConnected(true);
+ port.SetActiveController(0);
+ for (auto& controller : port.GetCompatibleControllers())
+ ActivateControllers(controller.GetHub());
+ }
+}
+
+void CGameClientInput::SetControllerLayouts(const ControllerVector& controllers)
+{
+ if (controllers.empty())
+ return;
+
+ for (const auto& controller : controllers)
+ {
+ const std::string controllerId = controller->ID();
+ if (m_controllerLayouts.find(controllerId) == m_controllerLayouts.end())
+ m_controllerLayouts[controllerId].reset(new CGameClientController(*this, controller));
+ }
+
+ std::vector<game_controller_layout> controllerStructs;
+ for (const auto& it : m_controllerLayouts)
+ controllerStructs.emplace_back(it.second->TranslateController());
+
+ try
+ {
+ m_struct.toAddon->SetControllerLayouts(&m_struct, controllerStructs.data(),
+ static_cast<unsigned int>(controllerStructs.size()));
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("SetControllerLayouts()");
+ }
+}
+
+const CControllerTree& CGameClientInput::GetDefaultControllerTree() const
+{
+ return m_topology->GetControllerTree();
+}
+
+const CControllerTree& CGameClientInput::GetActiveControllerTree() const
+{
+ return m_portManager->GetControllerTree();
+}
+
+bool CGameClientInput::SupportsKeyboard() const
+{
+ const CControllerTree& controllers = GetDefaultControllerTree();
+
+ auto it =
+ std::find_if(controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::KEYBOARD; });
+
+ return it != controllers.GetPorts().end() && !it->GetCompatibleControllers().empty();
+}
+
+bool CGameClientInput::SupportsMouse() const
+{
+ const CControllerTree& controllers = GetDefaultControllerTree();
+
+ auto it =
+ std::find_if(controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::MOUSE; });
+
+ return it != controllers.GetPorts().end() && !it->GetCompatibleControllers().empty();
+}
+
+int CGameClientInput::GetPlayerLimit() const
+{
+ return m_topology->GetPlayerLimit();
+}
+
+bool CGameClientInput::ConnectController(const std::string& portAddress,
+ const ControllerPtr& controller)
+{
+ // Validate parameters
+ if (portAddress.empty() || !controller)
+ return false;
+
+ const CControllerTree& controllerTree = GetDefaultControllerTree();
+
+ // Validate controller
+ const CPortNode& port = controllerTree.GetPort(portAddress);
+ if (!port.IsControllerAccepted(portAddress, controller->ID()))
+ {
+ CLog::Log(LOGERROR, "Failed to open port: Invalid controller \"{}\" on port \"{}\"",
+ controller->ID(), portAddress);
+ return false;
+ }
+
+ const CPortNode& currentPort = GetActiveControllerTree().GetPort(portAddress);
+
+ // Close current ports if any are open
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+ CloseJoysticks(currentPort, inputHandlingLock);
+ inputHandlingLock.reset();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (!m_gameClient.Initialized())
+ return false;
+
+ try
+ {
+ if (!m_struct.toAddon->ConnectController(&m_struct, true, portAddress.c_str(),
+ controller->ID().c_str()))
+ {
+ return false;
+ }
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("ConnectController()");
+ return false;
+ }
+ }
+
+ // Update port state
+ m_portManager->ConnectController(portAddress, true, controller->ID());
+ SetChanged();
+
+ // Update agent input
+ if (controller->Layout().Topology().ProvidesInput())
+ OpenJoystick(portAddress, controller);
+
+ bool bSuccess = true;
+
+ // If port is a multitap, we need to activate its children
+ const CPortNode& updatedPort = GetActiveControllerTree().GetPort(portAddress);
+ const PortVec& childPorts = updatedPort.GetActiveController().GetHub().GetPorts();
+ for (const CPortNode& childPort : childPorts)
+ {
+ const ControllerPtr& childController = childPort.GetActiveController().GetController();
+ if (childController)
+ bSuccess &= ConnectController(childPort.GetAddress(), childController);
+ }
+
+ return bSuccess;
+}
+
+bool CGameClientInput::DisconnectController(const std::string& portAddress)
+{
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+
+ // If port is a multitap, we need to deactivate its children
+ const CPortNode& currentPort = GetActiveControllerTree().GetPort(portAddress);
+ CloseJoysticks(currentPort, inputHandlingLock);
+
+ // If a port was closed, then destroying the lock will block until all
+ // peripheral input handling is complete to avoid invalidating the port's
+ // input handler
+ inputHandlingLock.reset();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (!m_gameClient.Initialized())
+ return false;
+
+ try
+ {
+ if (!m_struct.toAddon->ConnectController(&m_struct, false, portAddress.c_str(), ""))
+ return false;
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("ConnectController()");
+ return false;
+ }
+ }
+
+ // Update port state
+ m_portManager->ConnectController(portAddress, false);
+ SetChanged();
+
+ // Update agent input
+ CloseJoystick(portAddress, inputHandlingLock);
+ inputHandlingLock.reset();
+
+ return true;
+}
+
+void CGameClientInput::SavePorts()
+{
+ // Save port state
+ m_portManager->SaveXMLAsync();
+
+ // Let the observers know that ports have changed
+ NotifyObservers(ObservableMessageGamePortsChanged);
+}
+
+void CGameClientInput::ResetPorts()
+{
+ const CControllerTree& controllerTree = GetDefaultControllerTree();
+ for (const CPortNode& port : controllerTree.GetPorts())
+ ConnectController(port.GetAddress(), port.GetActiveController().GetController());
+}
+
+bool CGameClientInput::HasAgent() const
+{
+ if (!m_joysticks.empty())
+ return true;
+
+ if (m_keyboard)
+ return true;
+
+ if (m_mouse)
+ return true;
+
+ return false;
+}
+
+bool CGameClientInput::OpenKeyboard(const ControllerPtr& controller,
+ const PERIPHERALS::PeripheralPtr& keyboard)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open keyboard, no controller given");
+ return false;
+ }
+
+ if (!keyboard)
+ return false;
+
+ bool bSuccess = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ bSuccess = m_struct.toAddon->EnableKeyboard(&m_struct, true, controller->ID().c_str());
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableKeyboard()");
+ }
+ }
+ }
+
+ if (bSuccess)
+ {
+ m_keyboard =
+ std::make_unique<CGameClientKeyboard>(m_gameClient, controller->ID(), keyboard.get());
+ m_keyboard->SetSource(keyboard);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClientInput::IsKeyboardOpen() const
+{
+ return static_cast<bool>(m_keyboard);
+}
+
+void CGameClientInput::CloseKeyboard()
+{
+ if (m_keyboard)
+ {
+ m_keyboard.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ m_struct.toAddon->EnableKeyboard(&m_struct, false, "");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableKeyboard()");
+ }
+ }
+ }
+}
+
+bool CGameClientInput::OpenMouse(const ControllerPtr& controller,
+ const PERIPHERALS::PeripheralPtr& mouse)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open mouse, no controller given");
+ return false;
+ }
+
+ if (!mouse)
+ return false;
+
+ bool bSuccess = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ bSuccess = m_struct.toAddon->EnableMouse(&m_struct, true, controller->ID().c_str());
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableMouse()");
+ }
+ }
+ }
+
+ if (bSuccess)
+ {
+ m_mouse = std::make_unique<CGameClientMouse>(m_gameClient, controller->ID(), mouse.get());
+ m_mouse->SetSource(mouse);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClientInput::IsMouseOpen() const
+{
+ return static_cast<bool>(m_mouse);
+}
+
+void CGameClientInput::CloseMouse()
+{
+ if (m_mouse)
+ {
+ m_mouse.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ m_struct.toAddon->EnableMouse(&m_struct, false, "");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableMouse()");
+ }
+ }
+ }
+}
+
+bool CGameClientInput::OpenJoystick(const std::string& portAddress, const ControllerPtr& controller)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open port \"{}\", no controller given", portAddress);
+ return false;
+ }
+
+ if (m_joysticks.find(portAddress) != m_joysticks.end())
+ {
+ CLog::Log(LOGERROR, "Failed to open port \"{}\", already open", portAddress);
+ return false;
+ }
+
+ m_joysticks[portAddress].reset(new CGameClientJoystick(m_gameClient, portAddress, controller));
+
+ return true;
+}
+
+void CGameClientInput::CloseJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ std::vector<std::string> portAddresses;
+ for (const auto& it : m_joysticks)
+ portAddresses.emplace_back(it.first);
+
+ for (const std::string& portAddress : portAddresses)
+ CloseJoystick(portAddress, inputHandlingLock);
+}
+
+void CGameClientInput::CloseJoysticks(const CPortNode& port,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ const PortVec& childPorts = port.GetActiveController().GetHub().GetPorts();
+ for (const CPortNode& childPort : childPorts)
+ CloseJoysticks(childPort, inputHandlingLock);
+
+ CloseJoystick(port.GetAddress(), inputHandlingLock);
+}
+
+void CGameClientInput::CloseJoystick(const std::string& portAddress,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ auto it = m_joysticks.find(portAddress);
+ if (it != m_joysticks.end())
+ {
+ if (!inputHandlingLock)
+ {
+ // An input handler is being destroyed. Disable input until the lock is
+ // released. Note: acquiring the lock blocks until all peripheral input
+ // has been handled.
+ inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock();
+ }
+
+ m_joysticks.erase(it);
+ }
+}
+
+void CGameClientInput::HardwareReset()
+{
+ if (m_hardware)
+ m_hardware->OnResetButton();
+}
+
+bool CGameClientInput::ReceiveInputEvent(const game_input_event& event)
+{
+ bool bHandled = false;
+
+ switch (event.type)
+ {
+ case GAME_INPUT_EVENT_MOTOR:
+ if (event.port_address != nullptr && event.feature_name != nullptr)
+ bHandled = SetRumble(event.port_address, event.feature_name, event.motor.magnitude);
+ break;
+ default:
+ break;
+ }
+
+ return bHandled;
+}
+
+bool CGameClientInput::SetRumble(const std::string& portAddress,
+ const std::string& feature,
+ float magnitude)
+{
+ bool bHandled = false;
+
+ auto it = m_joysticks.find(portAddress);
+ if (it != m_joysticks.end())
+ bHandled = it->second->SetRumble(feature, magnitude);
+
+ return bHandled;
+}
+
+ControllerVector CGameClientInput::GetControllers(const CGameClient& gameClient)
+{
+ using namespace ADDON;
+
+ ControllerVector controllers;
+
+ CGameServices& gameServices = CServiceBroker::GetGameServices();
+
+ const auto& dependencies = gameClient.GetDependencies();
+ for (auto it = dependencies.begin(); it != dependencies.end(); ++it)
+ {
+ ControllerPtr controller = gameServices.GetController(it->id);
+ if (controller)
+ controllers.push_back(controller);
+ }
+
+ if (controllers.empty())
+ {
+ // Use the default controller
+ ControllerPtr controller = gameServices.GetDefaultController();
+ if (controller)
+ controllers.push_back(controller);
+ }
+
+ return controllers;
+}