diff options
Diffstat (limited to 'xbmc/games/addons/input/GameClientInput.cpp')
-rw-r--r-- | xbmc/games/addons/input/GameClientInput.cpp | 697 |
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; +} |