diff options
Diffstat (limited to 'xbmc/games/agents/GameAgentManager.cpp')
-rw-r--r-- | xbmc/games/agents/GameAgentManager.cpp | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/xbmc/games/agents/GameAgentManager.cpp b/xbmc/games/agents/GameAgentManager.cpp new file mode 100644 index 0000000..5b72fbf --- /dev/null +++ b/xbmc/games/agents/GameAgentManager.cpp @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2017-2022 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 "GameAgentManager.h" + +#include "ServiceBroker.h" +#include "games/addons/GameClient.h" +#include "games/addons/input/GameClientInput.h" +#include "games/addons/input/GameClientJoystick.h" +#include "input/InputManager.h" +#include "peripherals/EventLockHandle.h" +#include "peripherals/Peripherals.h" +#include "peripherals/devices/Peripheral.h" +#include "peripherals/devices/PeripheralJoystick.h" +#include "utils/log.h" + +#include <array> + +using namespace KODI; +using namespace GAME; + +CGameAgentManager::CGameAgentManager(PERIPHERALS::CPeripherals& peripheralManager, + CInputManager& inputManager) + : m_peripheralManager(peripheralManager), m_inputManager(inputManager) +{ + // Register callbacks + m_peripheralManager.RegisterObserver(this); + m_inputManager.RegisterKeyboardDriverHandler(this); + m_inputManager.RegisterMouseDriverHandler(this); +} + +CGameAgentManager::~CGameAgentManager() +{ + // Unregister callbacks in reverse order + m_inputManager.UnregisterMouseDriverHandler(this); + m_inputManager.UnregisterKeyboardDriverHandler(this); + m_peripheralManager.UnregisterObserver(this); +} + +void CGameAgentManager::Start(GameClientPtr gameClient) +{ + // Initialize state + m_gameClient = std::move(gameClient); + + // Register callbacks + if (m_gameClient) + m_gameClient->Input().RegisterObserver(this); +} + +void CGameAgentManager::Stop() +{ + // Unregister callbacks in reverse order + if (m_gameClient) + m_gameClient->Input().UnregisterObserver(this); + + // Close open joysticks + { + PERIPHERALS::EventLockHandlePtr inputHandlingLock; + + for (const auto& [inputProvider, joystick] : m_portMap) + { + if (!inputHandlingLock) + inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock(); + + joystick->UnregisterInput(inputProvider); + + SetChanged(true); + } + + m_portMap.clear(); + m_peripheralMap.clear(); + m_disconnectedPeripherals.clear(); + } + + // Notify observers if anything changed + NotifyObservers(ObservableMessageGameAgentsChanged); + + // Reset state + m_gameClient.reset(); +} + +void CGameAgentManager::Refresh() +{ + if (m_gameClient) + { + // Open keyboard + ProcessKeyboard(); + + // Open mouse + ProcessMouse(); + + // Open/close joysticks + PERIPHERALS::EventLockHandlePtr inputHandlingLock; + ProcessJoysticks(inputHandlingLock); + } + + // Notify observers if anything changed + NotifyObservers(ObservableMessageGameAgentsChanged); +} + +void CGameAgentManager::Notify(const Observable& obs, const ObservableMessage msg) +{ + switch (msg) + { + case ObservableMessageGamePortsChanged: + case ObservableMessagePeripheralsChanged: + { + Refresh(); + break; + } + default: + break; + } +} + +bool CGameAgentManager::OnKeyPress(const CKey& key) +{ + m_bHasKeyboard = true; + return false; +} + +bool CGameAgentManager::OnPosition(int x, int y) +{ + m_bHasMouse = true; + return false; +} + +bool CGameAgentManager::OnButtonPress(MOUSE::BUTTON_ID button) +{ + m_bHasMouse = true; + return false; +} + +void CGameAgentManager::ProcessJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock) +{ + // Get system joysticks. + // + // It's important to hold these shared pointers for the function scope + // because we call into the input handlers in m_portMap. + // + // The input handlers are upcasted from their peripheral object, so the + // peripheral object must persist through this function. + // + PERIPHERALS::PeripheralVector joysticks; + m_peripheralManager.GetPeripheralsWithFeature(joysticks, PERIPHERALS::FEATURE_JOYSTICK); + + // Remove "virtual" Android joysticks + // + // The heuristic used to identify these is to check if the device name is all + // lowercase letters and dashes (and contains at least one dash). The + // following virtual devices have been observed: + // + // shield-ask-remote + // sunxi-ir-uinput + // virtual-search + // + // Additionally, we specifically allow the following devices: + // + // virtual-remote + // + joysticks.erase( + std::remove_if(joysticks.begin(), joysticks.end(), + [](const PERIPHERALS::PeripheralPtr& joystick) + { + const std::string& joystickName = joystick->DeviceName(); + + // Skip joysticks in the allowlist + static const std::array<std::string, 1> peripheralAllowlist = { + "virtual-remote", + }; + if (std::find_if(peripheralAllowlist.begin(), peripheralAllowlist.end(), + [&joystickName](const std::string& allowedJoystick) { + return allowedJoystick == joystickName; + }) != peripheralAllowlist.end()) + { + return false; + } + + // Require at least one dash + if (std::find_if(joystickName.begin(), joystickName.end(), + [](char c) { return c == '-'; }) == joystickName.end()) + { + return false; + } + + // Require all lowercase letters or dashes + if (std::find_if(joystickName.begin(), joystickName.end(), + [](char c) + { + const bool isLowercase = ('a' <= c && c <= 'z'); + const bool isDash = (c == '-'); + return !(isLowercase || isDash); + }) != joystickName.end()) + { + return false; + } + + // Joystick matches the pattern, remove it + return true; + }), + joysticks.end()); + + // Update expired joysticks + UpdateExpiredJoysticks(joysticks, inputHandlingLock); + + // Perform the port mapping + PortMap newPortMap = + MapJoysticks(joysticks, m_gameClient->Input().GetJoystickMap(), m_currentPorts, + m_currentPeripherals, m_gameClient->Input().GetPlayerLimit()); + + // Update connected joysticks + std::set<PERIPHERALS::PeripheralPtr> disconnectedPeripherals; + UpdateConnectedJoysticks(joysticks, newPortMap, inputHandlingLock, disconnectedPeripherals); + + // Rebuild peripheral map + PeripheralMap peripheralMap; + for (const auto& [inputProvider, joystick] : m_portMap) + peripheralMap[joystick->GetControllerAddress()] = joystick->GetSource(); + + // Log peripheral map if there were any changes + if (peripheralMap != m_peripheralMap || disconnectedPeripherals != m_disconnectedPeripherals) + { + m_peripheralMap = std::move(peripheralMap); + m_disconnectedPeripherals = std::move(disconnectedPeripherals); + LogPeripheralMap(m_peripheralMap, m_disconnectedPeripherals); + } +} + +void CGameAgentManager::ProcessKeyboard() +{ + if (m_bHasKeyboard && m_gameClient->Input().SupportsKeyboard() && + !m_gameClient->Input().IsKeyboardOpen()) + { + PERIPHERALS::PeripheralVector keyboards; + CServiceBroker::GetPeripherals().GetPeripheralsWithFeature(keyboards, + PERIPHERALS::FEATURE_KEYBOARD); + if (!keyboards.empty()) + { + const CControllerTree& controllers = m_gameClient->Input().GetActiveControllerTree(); + + auto it = std::find_if( + controllers.GetPorts().begin(), controllers.GetPorts().end(), + [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::KEYBOARD; }); + + PERIPHERALS::PeripheralPtr keyboard = std::move(keyboards.at(0)); + m_gameClient->Input().OpenKeyboard(it->GetActiveController().GetController(), keyboard); + + SetChanged(true); + } + } +} + +void CGameAgentManager::ProcessMouse() +{ + if (m_bHasMouse && m_gameClient->Input().SupportsMouse() && !m_gameClient->Input().IsMouseOpen()) + { + PERIPHERALS::PeripheralVector mice; + CServiceBroker::GetPeripherals().GetPeripheralsWithFeature(mice, PERIPHERALS::FEATURE_MOUSE); + if (!mice.empty()) + { + const CControllerTree& controllers = m_gameClient->Input().GetActiveControllerTree(); + + auto it = std::find_if( + controllers.GetPorts().begin(), controllers.GetPorts().end(), + [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::MOUSE; }); + + PERIPHERALS::PeripheralPtr mouse = std::move(mice.at(0)); + m_gameClient->Input().OpenMouse(it->GetActiveController().GetController(), mouse); + + SetChanged(true); + } + } +} + +void CGameAgentManager::UpdateExpiredJoysticks(const PERIPHERALS::PeripheralVector& joysticks, + PERIPHERALS::EventLockHandlePtr& inputHandlingLock) +{ + // Make a copy - expired joysticks are removed from m_portMap + PortMap portMapCopy = m_portMap; + + for (const auto& [inputProvider, joystick] : portMapCopy) + { + // Structured binding cannot be captured, so make a copy + JOYSTICK::IInputProvider* const inputProviderCopy = inputProvider; + + // Search peripheral vector for input provider + auto it2 = std::find_if(joysticks.begin(), joysticks.end(), + [inputProviderCopy](const PERIPHERALS::PeripheralPtr& joystick) { + // Upcast peripheral to input interface + JOYSTICK::IInputProvider* peripheralInput = joystick.get(); + + // Compare + return inputProviderCopy == peripheralInput; + }); + + // If peripheral wasn't found, then it was disconnected + const bool bDisconnected = (it2 == joysticks.end()); + + // Erase joystick from port map if its peripheral becomes disconnected + if (bDisconnected) + { + // Must use nullptr because peripheral has likely fallen out of scope, + // destroying the object + joystick->UnregisterInput(nullptr); + + if (!inputHandlingLock) + inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock(); + m_portMap.erase(inputProvider); + + SetChanged(true); + } + } +} + +void CGameAgentManager::UpdateConnectedJoysticks( + const PERIPHERALS::PeripheralVector& joysticks, + const PortMap& newPortMap, + PERIPHERALS::EventLockHandlePtr& inputHandlingLock, + std::set<PERIPHERALS::PeripheralPtr>& disconnectedPeripherals) +{ + for (auto& peripheralJoystick : joysticks) + { + // Upcast peripheral to input interface + JOYSTICK::IInputProvider* inputProvider = peripheralJoystick.get(); + + // Get connection states + auto itConnectedPort = newPortMap.find(inputProvider); + auto itDisconnectedPort = m_portMap.find(inputProvider); + + // Get possibly connected joystick + std::shared_ptr<CGameClientJoystick> newJoystick; + if (itConnectedPort != newPortMap.end()) + newJoystick = itConnectedPort->second; + + // Get possibly disconnected joystick + std::shared_ptr<CGameClientJoystick> oldJoystick; + if (itDisconnectedPort != m_portMap.end()) + oldJoystick = itDisconnectedPort->second; + + // Check for a change in joysticks + if (oldJoystick != newJoystick) + { + // Unregister old input handler + if (oldJoystick != nullptr) + { + oldJoystick->UnregisterInput(inputProvider); + + if (!inputHandlingLock) + inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock(); + m_portMap.erase(itDisconnectedPort); + + SetChanged(true); + } + + // Register new handler + if (newJoystick != nullptr) + { + newJoystick->RegisterInput(inputProvider); + + m_portMap[inputProvider] = std::move(newJoystick); + + SetChanged(true); + } + } + } + + // Record disconnected peripherals + for (const auto& peripheral : joysticks) + { + // Upcast peripheral to input interface + JOYSTICK::IInputProvider* inputProvider = peripheral.get(); + + // Check if peripheral is disconnected + if (m_portMap.find(inputProvider) == m_portMap.end()) + disconnectedPeripherals.emplace(peripheral); + } +} + +CGameAgentManager::PortMap CGameAgentManager::MapJoysticks( + const PERIPHERALS::PeripheralVector& peripheralJoysticks, + const JoystickMap& gameClientjoysticks, + CurrentPortMap& currentPorts, + CurrentPeripheralMap& currentPeripherals, + int playerLimit) +{ + PortMap result; + + // First, create a map of the current ports to attempt to preserve + // player numbers + for (const auto& [portAddress, joystick] : gameClientjoysticks) + { + std::string sourceLocation = joystick->GetSourceLocation(); + if (!sourceLocation.empty()) + currentPorts[portAddress] = std::move(sourceLocation); + } + + // Allow reverse lookups by peripheral location + for (const auto& [portAddress, sourceLocation] : currentPorts) + currentPeripherals[sourceLocation] = portAddress; + + // Next, create a list of joystick peripherals sorted by order of last + // button press. Joysticks without a current port are assigned in this + // order. + PERIPHERALS::PeripheralVector availableJoysticks = peripheralJoysticks; + std::sort(availableJoysticks.begin(), availableJoysticks.end(), + [](const PERIPHERALS::PeripheralPtr& lhs, const PERIPHERALS::PeripheralPtr& rhs) { + if (lhs->LastActive().IsValid() && !rhs->LastActive().IsValid()) + return true; + if (!lhs->LastActive().IsValid() && rhs->LastActive().IsValid()) + return false; + + return lhs->LastActive() > rhs->LastActive(); + }); + + // Loop through the active ports and assign joysticks + unsigned int iJoystick = 0; + for (const auto& [portAddress, gameClientJoystick] : gameClientjoysticks) + { + const unsigned int joystickCount = ++iJoystick; + + // Check if we're out of joystick peripherals or over the topology limit + if (availableJoysticks.empty() || + (playerLimit >= 0 && static_cast<int>(joystickCount) > playerLimit)) + { + gameClientJoystick->ClearSource(); + continue; + } + + PERIPHERALS::PeripheralVector::iterator itJoystick = availableJoysticks.end(); + + // Attempt to preserve player numbers + auto itCurrentPort = currentPorts.find(portAddress); + if (itCurrentPort != currentPorts.end()) + { + const PeripheralLocation& currentPeripheral = itCurrentPort->second; + + // Find peripheral with matching source location + itJoystick = std::find_if(availableJoysticks.begin(), availableJoysticks.end(), + [¤tPeripheral](const PERIPHERALS::PeripheralPtr& joystick) { + return joystick->Location() == currentPeripheral; + }); + } + + if (itJoystick == availableJoysticks.end()) + { + // Get the next most recently active joystick that doesn't have a current port + itJoystick = std::find_if( + availableJoysticks.begin(), availableJoysticks.end(), + [¤tPeripherals, &gameClientjoysticks](const PERIPHERALS::PeripheralPtr& joystick) { + const PeripheralLocation& joystickLocation = joystick->Location(); + + // If joystick doesn't have a current port, use it + auto itPeripheral = currentPeripherals.find(joystickLocation); + if (itPeripheral == currentPeripherals.end()) + return true; + + // Get the address of the last port this joystick was connected to + const PortAddress& portAddress = itPeripheral->second; + + // If port is disconnected, use this joystick + if (gameClientjoysticks.find(portAddress) == gameClientjoysticks.end()) + return true; + + return false; + }); + } + + // If found, assign the port and remove from the lists + if (itJoystick != availableJoysticks.end()) + { + // Dereference iterator + PERIPHERALS::PeripheralPtr peripheralJoystick = *itJoystick; + + // Map joystick + MapJoystick(std::move(peripheralJoystick), gameClientJoystick, result); + + // Remove from availableJoysticks list + availableJoysticks.erase(itJoystick); + } + else + { + // No joystick found, clear the port + gameClientJoystick->ClearSource(); + } + } + + return result; +} + +void CGameAgentManager::MapJoystick(PERIPHERALS::PeripheralPtr peripheralJoystick, + std::shared_ptr<CGameClientJoystick> gameClientJoystick, + PortMap& result) +{ + // Upcast peripheral joystick to input provider + JOYSTICK::IInputProvider* inputProvider = peripheralJoystick.get(); + + // Update game joystick's source peripheral + gameClientJoystick->SetSource(std::move(peripheralJoystick)); + + // Map input provider to input handler + result[inputProvider] = std::move(gameClientJoystick); +} + +void CGameAgentManager::LogPeripheralMap( + const PeripheralMap& peripheralMap, + const std::set<PERIPHERALS::PeripheralPtr>& disconnectedPeripherals) +{ + CLog::Log(LOGDEBUG, "===== Peripheral Map ====="); + + unsigned int line = 0; + + if (!peripheralMap.empty()) + { + for (const auto& [controllerAddress, peripheral] : peripheralMap) + { + if (line != 0) + CLog::Log(LOGDEBUG, ""); + CLog::Log(LOGDEBUG, "{}:", controllerAddress); + CLog::Log(LOGDEBUG, " {} [{}]", peripheral->Location(), peripheral->DeviceName()); + + ++line; + } + } + + if (!disconnectedPeripherals.empty()) + { + if (line != 0) + CLog::Log(LOGDEBUG, ""); + CLog::Log(LOGDEBUG, "Disconnected:"); + + // Sort by peripheral location + std::map<std::string, std::string> disconnectedPeripheralMap; + for (const auto& peripheral : disconnectedPeripherals) + disconnectedPeripheralMap[peripheral->Location()] = peripheral->DeviceName(); + + // Log location and device name for disconnected peripherals + for (const auto& [location, deviceName] : disconnectedPeripheralMap) + CLog::Log(LOGDEBUG, " {} [{}]", location, deviceName); + } + + CLog::Log(LOGDEBUG, "=========================="); +} |