/* * 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 #include 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()) { } 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 controllerStructs; for (const auto& it : m_controllerLayouts) controllerStructs.emplace_back(it.second->TranslateController()); try { m_struct.toAddon->SetControllerLayouts(&m_struct, controllerStructs.data(), static_cast(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 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 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 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(m_gameClient, controller->ID(), keyboard.get()); m_keyboard->SetSource(keyboard); return true; } return false; } bool CGameClientInput::IsKeyboardOpen() const { return static_cast(m_keyboard); } void CGameClientInput::CloseKeyboard() { if (m_keyboard) { m_keyboard.reset(); std::unique_lock 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 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(m_gameClient, controller->ID(), mouse.get()); m_mouse->SetSource(mouse); return true; } return false; } bool CGameClientInput::IsMouseOpen() const { return static_cast(m_mouse); } void CGameClientInput::CloseMouse() { if (m_mouse) { m_mouse.reset(); std::unique_lock 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 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; }