diff --git a/xbmc/ServiceManager.cpp b/xbmc/ServiceManager.cpp index a370ccf20dda8..af22d934d0211 100644 --- a/xbmc/ServiceManager.cpp +++ b/xbmc/ServiceManager.cpp @@ -203,6 +203,7 @@ bool CServiceManager::InitStageThree(const std::shared_ptr& pro m_gameServices = std::make_unique( *m_gameControllerManager, *m_gameRenderManager, *m_peripherals, *profileManager, *m_inputManager, *m_addonMgr); + m_gameServices->Initialize(); m_contextMenuManager->Init(); @@ -229,6 +230,7 @@ void CServiceManager::DeinitStageThree() m_playerCoreFactory.reset(); m_PVRManager->Deinit(); m_contextMenuManager->Deinit(); + m_gameServices->Deinitialize(); m_gameServices.reset(); m_peripherals->Clear(); diff --git a/xbmc/games/GameServices.cpp b/xbmc/games/GameServices.cpp index 946ec63ab8830..be5cdbc000732 100644 --- a/xbmc/games/GameServices.cpp +++ b/xbmc/games/GameServices.cpp @@ -35,6 +35,18 @@ CGameServices::CGameServices(CControllerManager& controllerManager, CGameServices::~CGameServices() = default; +void CGameServices::Initialize() +{ + // Must not call this from the constructor because the controller tree + // calls back into CGameServices to get controller profiles + m_agentInput->Initialize(); +} + +void CGameServices::Deinitialize() +{ + m_agentInput->Deinitialize(); +} + ControllerPtr CGameServices::GetController(const std::string& controllerId) { return m_controllerManager.GetController(controllerId); diff --git a/xbmc/games/GameServices.h b/xbmc/games/GameServices.h index 810d78e898c86..5ed0075f8079b 100644 --- a/xbmc/games/GameServices.h +++ b/xbmc/games/GameServices.h @@ -58,6 +58,11 @@ class CGameServices ADDON::CAddonMgr& addons); ~CGameServices(); + // Lifecycle functions + void Initialize(); + void Deinitialize(); + + // Controller accessors ControllerPtr GetController(const std::string& controllerId); ControllerPtr GetDefaultController(); ControllerPtr GetDefaultKeyboard(); diff --git a/xbmc/games/agents/input/AgentInput.cpp b/xbmc/games/agents/input/AgentInput.cpp index 099f0a7cce034..91bed60c86409 100644 --- a/xbmc/games/agents/input/AgentInput.cpp +++ b/xbmc/games/agents/input/AgentInput.cpp @@ -9,6 +9,7 @@ #include "AgentInput.h" #include "AgentController.h" +#include "AgentInputMap.h" #include "games/addons/GameClient.h" #include "games/addons/input/GameClientInput.h" #include "games/addons/input/GameClientJoystick.h" @@ -26,20 +27,35 @@ using namespace KODI; using namespace GAME; CAgentInput::CAgentInput(PERIPHERALS::CPeripherals& peripheralManager, CInputManager& inputManager) - : m_peripheralManager(peripheralManager), m_inputManager(inputManager) + : m_peripheralManager(peripheralManager), + m_inputManager(inputManager), + m_inputMap(std::make_unique()) { +} + +CAgentInput::~CAgentInput() = default; + +void CAgentInput::Initialize() +{ + // Load input map + //! @todo Load async to not block main thread during app initialization + m_inputMap->LoadXML(); + // Register callbacks m_peripheralManager.RegisterObserver(this); m_inputManager.RegisterKeyboardDriverHandler(this); m_inputManager.RegisterMouseDriverHandler(this); } -CAgentInput::~CAgentInput() +void CAgentInput::Deinitialize() { // Unregister callbacks in reverse order m_inputManager.UnregisterMouseDriverHandler(this); m_inputManager.UnregisterKeyboardDriverHandler(this); m_peripheralManager.UnregisterObserver(this); + + // Clear input map + m_inputMap->Clear(); } void CAgentInput::Start(GameClientPtr gameClient) @@ -53,6 +69,9 @@ void CAgentInput::Start(GameClientPtr gameClient) // Perform initial refresh Refresh(); + + // Update input map + m_inputMap->AddGameClient(m_gameClient); } void CAgentInput::Stop() diff --git a/xbmc/games/agents/input/AgentInput.h b/xbmc/games/agents/input/AgentInput.h index 1de3701132b7b..62768646c6e68 100644 --- a/xbmc/games/agents/input/AgentInput.h +++ b/xbmc/games/agents/input/AgentInput.h @@ -45,6 +45,7 @@ class IMouseInputProvider; namespace GAME { +class CAgentInputMap; class CGameClient; class CGameClientJoystick; @@ -73,6 +74,8 @@ class CAgentInput : public Observable, virtual ~CAgentInput(); // Lifecycle functions + void Initialize(); + void Deinitialize(); void Start(GameClientPtr gameClient); void Stop(); void Refresh(); @@ -204,6 +207,11 @@ class CAgentInput : public Observable, * Source peripherals are not exposed to the game. */ std::set m_disconnectedPeripherals; + + /*! + * \brief Input map for the agents + */ + std::unique_ptr m_inputMap; }; } // namespace GAME } // namespace KODI diff --git a/xbmc/games/agents/input/AgentInputMap.cpp b/xbmc/games/agents/input/AgentInputMap.cpp new file mode 100644 index 0000000000000..c4e4817814772 --- /dev/null +++ b/xbmc/games/agents/input/AgentInputMap.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024 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 "AgentInputMap.h" + +#include "AgentInputMapXML.h" +#include "AgentTopology.h" +#include "URL.h" +#include "games/addons/GameClient.h" +#include "utils/FileUtils.h" +#include "utils/URIUtils.h" +#include "utils/XBMCTinyXML2.h" +#include "utils/log.h" + +#include + +using namespace KODI; +using namespace GAME; + +namespace +{ +// Application parameters +constexpr auto PROFILE_ROOT = "special://masterprofile"; + +// Game API parameters +constexpr auto TOPOLOGY_XML_FILE = "topology.xml"; + +// Input parameters +constexpr auto TOPOLOGIES_XML_FILE = "gametopologies.xml"; //! @todo Move to "games" subfolder +} // namespace + +CAgentInputMap::CAgentInputMap() + : m_topologiesXmlPath{URIUtils::AddFileToFolder(PROFILE_ROOT, TOPOLOGIES_XML_FILE)} +{ +} + +CAgentInputMap::~CAgentInputMap() +{ + // Wait for save tasks + for (std::future& task : m_saveFutures) + task.wait(); + m_saveFutures.clear(); +} + +void CAgentInputMap::Clear() +{ + // Clear input parameters + m_topologiesById.clear(); + m_topologiesByDigest.clear(); +}; + +const CControllerTree& CAgentInputMap::GetAgentTopology(unsigned int topologyId) const +{ + const auto it = m_topologiesById.find(topologyId); + if (it != m_topologiesById.end()) + return it->second->GetControllerTree(); + + static const CControllerTree empty; + return empty; +} + +bool CAgentInputMap::AddGameClient(const GameClientPtr& gameClient) +{ + // Get path to game client's topologies.xml file + std::string topologyXmlSharePath = URIUtils::AddFileToFolder( + gameClient->Path(), GAME_CLIENT_RESOURCES_DIRECTORY, TOPOLOGY_XML_FILE); + std::string topologyXmlLibPath = URIUtils::AddFileToFolder( + gameClient->LibPath(), GAME_CLIENT_RESOURCES_DIRECTORY, TOPOLOGY_XML_FILE); + + std::string topologyXmlPath = topologyXmlSharePath; + if (!CFileUtils::Exists(topologyXmlPath)) + topologyXmlPath = topologyXmlLibPath; + + if (!CFileUtils::Exists(topologyXmlPath)) + { + CLog::Log(LOGDEBUG, "Can't load topologies, file doesn't exist"); + CLog::Log(LOGDEBUG, " Tried: {}", CURL::GetRedacted(topologyXmlSharePath)); + CLog::Log(LOGDEBUG, " Tried: {}", CURL::GetRedacted(topologyXmlLibPath)); + return false; + } + + CLog::Log(LOGINFO, "{}: Loading topology: {}", gameClient->ID(), + CURL::GetRedacted(topologyXmlPath)); + + // Load topology + CXBMCTinyXML2 xmlDoc; + if (!xmlDoc.LoadFile(topologyXmlPath)) + { + CLog::Log(LOGDEBUG, "Unable to load file: {} at line {}", xmlDoc.ErrorStr(), + xmlDoc.ErrorLineNum()); + return false; + } + + // Deserialize topology + std::shared_ptr agentTopology = std::make_shared(); + if (!agentTopology->DeserializeControllerTree(xmlDoc)) + return false; + + // Get topology digest + agentTopology->UpdateDigest(); + const std::string& digest = agentTopology->GetDigest(); + + // Check if topology digest already exists + auto it = m_topologiesByDigest.find(digest); + if (it != m_topologiesByDigest.end()) + { + // Dereference iterator + CAgentTopology& existingTopology = *it->second; + + // Add game client to existing topology + const std::set& gameClients = existingTopology.GetGameClients(); + if (gameClients.find(gameClient->ID()) == gameClients.end()) + { + // Add game client + existingTopology.AddGameClient(gameClient->ID()); + + // Save topologies + SaveXMLAsync(); + } + } + else + { + // Calculate next topology ID + unsigned int nextTopologyId = 0; + if (!m_topologiesById.empty()) + nextTopologyId = m_topologiesById.rbegin()->first + 1; + + // Set topology ID + agentTopology->SetID(nextTopologyId); + + // Set first game client + agentTopology->AddGameClient(gameClient->ID()); + + // Add topology + m_topologiesById[agentTopology->GetID()] = agentTopology; + m_topologiesByDigest[agentTopology->GetDigest()] = std::move(agentTopology); + + // Save topologies + SaveXMLAsync(); + } + + return true; +} + +bool CAgentInputMap::LoadXML() +{ + Clear(); + + // Check if topologies.xml file exists + if (!CFileUtils::Exists(m_topologiesXmlPath)) + { + CLog::Log(LOGDEBUG, "Can't load topologies, file doesn't exist: \"{}\"", + CURL::GetRedacted(m_topologiesXmlPath)); + return false; + } + + CLog::Log(LOGINFO, "Loading topologies: {}", CURL::GetRedacted(m_topologiesXmlPath)); + + // Load topologies + CXBMCTinyXML2 xmlDoc; + if (!xmlDoc.LoadFile(m_topologiesXmlPath)) + { + CLog::Log(LOGDEBUG, "Unable to load file: {} at line {}", xmlDoc.ErrorStr(), + xmlDoc.ErrorLineNum()); + return false; + } + + // Deserialize topologies + if (!CAgentInputMapXML::DeserializeTopologies(xmlDoc, m_topologiesById, m_topologiesByDigest)) + return false; + + return true; +} + +void CAgentInputMap::SaveXMLAsync() +{ + TopologyIDMap topologies = m_topologiesById; + + // Prune any finished save tasks + m_saveFutures.erase(std::remove_if(m_saveFutures.begin(), m_saveFutures.end(), + [](std::future& task) { + return task.wait_for(std::chrono::seconds(0)) == + std::future_status::ready; + }), + m_saveFutures.end()); + + // Save async + std::future task = std::async(std::launch::async, + [this, topologies = std::move(topologies)]() + { + CLog::Log(LOGDEBUG, "Saving topologies to {}", + CURL::GetRedacted(m_topologiesXmlPath)); + + CXBMCTinyXML2 doc; + if (CAgentInputMapXML::SerializeTopologies(doc, topologies)) + { + std::lock_guard lock(m_saveMutex); + doc.SaveFile(m_topologiesXmlPath); + } + }); + + m_saveFutures.emplace_back(std::move(task)); +} diff --git a/xbmc/games/agents/input/AgentInputMap.h b/xbmc/games/agents/input/AgentInputMap.h new file mode 100644 index 0000000000000..3f0580bbdeb98 --- /dev/null +++ b/xbmc/games/agents/input/AgentInputMap.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 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 "games/GameTypes.h" +#include "games/controllers/types/ControllerTree.h" + +#include +#include +#include +#include +#include +#include + +namespace tinyxml2 +{ +class XMLElement; +} // namespace tinyxml2 + +namespace KODI +{ +namespace GAME +{ +class CAgentTopology; + +/*! + * \ingroup games + */ +class CAgentInputMap +{ +public: + /*! + * \brief ID -> topology map + */ + using TopologyIDMap = std::map>; + + /*! + * \brief Digest -> topology map + */ + using TopologyDigestMap = std::map>; + + CAgentInputMap(); + ~CAgentInputMap(); + + /*! + * \brief Clean the input map + */ + void Clear(); + + /*! + * \brief Get topology by ID + */ + const CControllerTree& GetAgentTopology(unsigned int topologyId) const; + + /*! + * \brief Add a game client to the input map + */ + bool AddGameClient(const GameClientPtr& gameClient); + + // XML functions + bool LoadXML(); + void SaveXMLAsync(); + +private: + // Filesystem parameters + std::string m_topologiesXmlPath; + std::vector> m_saveFutures; + std::mutex m_saveMutex; + + // Input parameters + TopologyIDMap m_topologiesById; + TopologyDigestMap m_topologiesByDigest; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/agents/input/AgentInputMapXML.cpp b/xbmc/games/agents/input/AgentInputMapXML.cpp new file mode 100644 index 0000000000000..f3c1bae06848c --- /dev/null +++ b/xbmc/games/agents/input/AgentInputMapXML.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 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 "AgentInputMapXML.h" + +#include "AgentTopology.h" +#include "utils/log.h" + +#include + +using namespace KODI; +using namespace GAME; + +namespace +{ +constexpr auto XML_ELM_TOPOLOGIES = "topologies"; +constexpr auto XML_ELM_TOPOLOGY = "logicaltopology"; +} // namespace + +bool CAgentInputMapXML::SerializeTopologies(CXBMCTinyXML2& doc, + const CAgentInputMap::TopologyIDMap& topologies) +{ + // Create root topologies element + tinyxml2::XMLElement* topologiesElement = doc.NewElement(XML_ELM_TOPOLOGIES); + if (topologiesElement == nullptr) + return false; + + // Iterate over all topologies + for (const auto& topology : topologies) + { + // Dereference iterator + const CAgentTopology& agentTopology = *topology.second; + + // Create topology element + tinyxml2::XMLElement* topologyElement = + topologiesElement->InsertNewChildElement(XML_ELM_TOPOLOGY); + + // Serialize topology + if (!SerializeTopology(*topologyElement, agentTopology)) + continue; + } + + // Add topologies element + doc.InsertEndChild(topologiesElement); + + return true; +} + +bool CAgentInputMapXML::SerializeTopology(tinyxml2::XMLElement& topologyElement, + const CAgentTopology& topology) +{ + return topology.SerializeTopology(topologyElement); +} + +bool CAgentInputMapXML::DeserializeTopologies(const CXBMCTinyXML2& xmlDoc, + CAgentInputMap::TopologyIDMap& topologiesById, + CAgentInputMap::TopologyDigestMap& topologiesByDigest) +{ + // Validate root element + const tinyxml2::XMLElement* topologiesElement = xmlDoc.RootElement(); + if (topologiesElement == nullptr || + std::strcmp(topologiesElement->Value(), XML_ELM_TOPOLOGIES) != 0) + { + CLog::Log(LOGERROR, "Can't find root <{}> tag", XML_ELM_TOPOLOGIES); + return false; + } + + // Get first "logicaltopology" element + const tinyxml2::XMLElement* topologyElement = + topologiesElement->FirstChildElement(XML_ELM_TOPOLOGY); + if (topologyElement == nullptr) + { + CLog::Log(LOGERROR, "Missing element <{}>", XML_ELM_TOPOLOGY); + return false; + } + + // Iterate over all "logicaltopology" elements + while (topologyElement != nullptr) + { + // Create topology + std::shared_ptr topology = std::make_shared(); + if (DeserializeTopology(*topologyElement, *topology)) + { + // Check topology ID + if (topologiesById.find(topology->GetID()) != topologiesById.end()) + { + CLog::Log(LOGERROR, "Duplicate topology ID with ID {} and digest {}", topology->GetID(), + topology->GetDigest()); + } + else + { + // Add topology + topologiesById[topology->GetID()] = topology; + topologiesByDigest[topology->GetDigest()] = std::move(topology); + } + } + + // Get next "logicaltopology" element + topologyElement = topologyElement->NextSiblingElement(XML_ELM_TOPOLOGY); + } + + return true; +} + +bool CAgentInputMapXML::DeserializeTopology(const tinyxml2::XMLElement& topologyElement, + CAgentTopology& topology) +{ + return topology.DeserializeTopology(topologyElement); +} diff --git a/xbmc/games/agents/input/AgentInputMapXML.h b/xbmc/games/agents/input/AgentInputMapXML.h new file mode 100644 index 0000000000000..fd131b0ae5740 --- /dev/null +++ b/xbmc/games/agents/input/AgentInputMapXML.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 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 "AgentInputMap.h" +#include "utils/XBMCTinyXML2.h" + +namespace KODI +{ +namespace GAME +{ +class CAgentTopology; + +/*! + * \ingroup games + */ +class CAgentInputMapXML +{ +public: + static bool SerializeTopologies(CXBMCTinyXML2& xmlDoc, + const CAgentInputMap::TopologyIDMap& topologies); + static bool SerializeTopology(tinyxml2::XMLElement& topologyElement, + const CAgentTopology& topology); + + static bool DeserializeTopologies(const CXBMCTinyXML2& xmlDoc, + CAgentInputMap::TopologyIDMap& topologiesById, + CAgentInputMap::TopologyDigestMap& topologiesByDigest); + static bool DeserializeTopology(const tinyxml2::XMLElement& topologyElement, + CAgentTopology& topology); +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/agents/input/AgentTopology.cpp b/xbmc/games/agents/input/AgentTopology.cpp new file mode 100644 index 0000000000000..1d8ad898a97bb --- /dev/null +++ b/xbmc/games/agents/input/AgentTopology.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 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 "AgentTopology.h" + +#include "AgentTopologyXML.h" +#include "utils/Digest.h" + +using namespace KODI; +using namespace GAME; + +CAgentTopology::CAgentTopology() : m_controllerTree(std::make_unique()) +{ +} + +void CAgentTopology::Clear() +{ + m_controllerTree->Clear(); + m_gameClients.clear(); +} + +void CAgentTopology::UpdateDigest() +{ + if (m_digest.empty() || !m_digestCreationUtc.IsValid()) + { + // Calculate digest + std::string strDigest = m_controllerTree->GetDigest(UTILITY::CDigest::Type::SHA256); + m_digest = StringUtils::ToHexadecimal(strDigest); + + // Take a timestamp of the system clock + m_digestCreationUtc = CDateTime::GetUTCDateTime(); + } +} + +void CAgentTopology::SetGameClients(std::set gameClients) +{ + m_gameClients = std::move(gameClients); +} + +void CAgentTopology::AddGameClient(std::string gameClientId) +{ + m_gameClients.insert(std::move(gameClientId)); +} + +bool CAgentTopology::SerializeTopology(tinyxml2::XMLElement& topologyElement) const +{ + return CAgentTopologyXML::SerializeTopology(topologyElement, *this); +} + +bool CAgentTopology::DeserializeTopology(const tinyxml2::XMLElement& topologyElement) +{ + return CAgentTopologyXML::DeserializeTopology(topologyElement, *this); +} + +bool CAgentTopology::DeserializeControllerTree(const CXBMCTinyXML2& xmlDoc) +{ + return CAgentTopologyXML::DeserializeControllerTree(xmlDoc, *m_controllerTree); +} + +bool CAgentTopology::DeserializeControllerTree(const tinyxml2::XMLElement& topologyElement) +{ + return CAgentTopologyXML::DeserializeControllerTree(topologyElement, *m_controllerTree); +} + +bool CAgentTopology::DeserializeGameClients(const tinyxml2::XMLElement& topologyElement) +{ + return CAgentTopologyXML::DeserializeGameClients(topologyElement, m_gameClients); +} diff --git a/xbmc/games/agents/input/AgentTopology.h b/xbmc/games/agents/input/AgentTopology.h new file mode 100644 index 0000000000000..59a7b32332f24 --- /dev/null +++ b/xbmc/games/agents/input/AgentTopology.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 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 "XBDateTime.h" +#include "games/controllers/types/ControllerTree.h" +#include "utils/XBMCTinyXML2.h" + +#include +#include +#include + +namespace tinyxml2 +{ +class XMLElement; +} + +namespace KODI +{ +namespace GAME +{ +/*! + * \ingroup games + */ +class CAgentTopology +{ +public: + CAgentTopology(); + ~CAgentTopology() = default; + + void Clear(); + void UpdateDigest(); + + // Topology property accessors + unsigned int GetID() const { return m_id; } + const std::string& GetDigest() const { return m_digest; } + const CDateTime& GetDigestCreationUTC() const { return m_digestCreationUtc; } + + // Topology property mutators + void SetID(unsigned int id) { m_id = id; } + void SetDigest(const std::string& digest) { m_digest = digest; } + void SetDigestCreationUTC(const CDateTime& digestCreationUtc) + { + m_digestCreationUtc = digestCreationUtc; + } + + // Topology definition accessors + const CControllerTree& GetControllerTree() const { return *m_controllerTree; } + + // Game property accessors + const std::set& GetGameClients() const { return m_gameClients; } + + // Game property mutators + void SetGameClients(std::set gameClients); + void AddGameClient(std::string gameClientId); + + // XML functions + bool SerializeTopology(tinyxml2::XMLElement& topologyElement) const; + bool DeserializeTopology(const tinyxml2::XMLElement& topologyElement); + bool DeserializeControllerTree(const CXBMCTinyXML2& xmlDoc); + bool DeserializeControllerTree(const tinyxml2::XMLElement& topologyElement); + bool DeserializeGameClients(const tinyxml2::XMLElement& topologyElement); + +private: + // Topology properties + unsigned int m_id{0}; + std::string m_digest; + CDateTime m_digestCreationUtc; + + // Topology definition + std::unique_ptr m_controllerTree; + + // Game properties + std::set m_gameClients; +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/agents/input/AgentTopologyXML.cpp b/xbmc/games/agents/input/AgentTopologyXML.cpp new file mode 100644 index 0000000000000..172ed70e48f5e --- /dev/null +++ b/xbmc/games/agents/input/AgentTopologyXML.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2024 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 "AgentTopologyXML.h" + +#include "AgentTopology.h" +#include "utils/log.h" + +#include + +using namespace KODI; +using namespace GAME; + +namespace +{ +constexpr auto XML_ELM_TOPOLOGY = "logicaltopology"; +constexpr auto XML_ELM_DEFINITION = "definition"; +constexpr auto XML_ELM_GAMECLIENTS = "gameclients"; +constexpr auto XML_ELM_GAMECLIENT = "gameclient"; +constexpr auto XML_ATTR_TOPOLOGY_ID = "id"; +constexpr auto XML_ATTR_SHA2_256 = "sha2-256"; +constexpr auto XML_ATTR_DIGTEST_CREATION = "digestcreation"; +} // namespace + +bool CAgentTopologyXML::SerializeTopology(tinyxml2::XMLElement& topologyElement, + const CAgentTopology& topology) +{ + // Set attribute "id" to ID + topologyElement.SetAttribute(XML_ATTR_TOPOLOGY_ID, topology.GetID()); + + // Set attribute "sha2-256" to digest + topologyElement.SetAttribute(XML_ATTR_SHA2_256, topology.GetDigest().c_str()); + + // Set attribute "digestcreation" to digest creation date + topologyElement.SetAttribute(XML_ATTR_DIGTEST_CREATION, + topology.GetDigestCreationUTC().GetAsW3CDateTime(true).c_str()); + + // Create "definition" element + tinyxml2::XMLElement* definitionElement = + topologyElement.InsertNewChildElement(XML_ELM_DEFINITION); + + // Serialize controller tree + if (!topology.GetControllerTree().Serialize(*definitionElement)) + return false; + + // Create "gameclients" element + tinyxml2::XMLElement* gameClientsElement = + topologyElement.InsertNewChildElement(XML_ELM_GAMECLIENTS); + + // Serialize game clients + const std::set& gameClients = topology.GetGameClients(); + for (const std::string& gameClient : gameClients) + { + // Create "gameclient" element + tinyxml2::XMLElement* gameClientElement = + gameClientsElement->InsertNewChildElement(XML_ELM_GAMECLIENT); + + // Set value to ID of the game client + gameClientElement->SetText(gameClient.c_str()); + } + + return true; +} + +bool CAgentTopologyXML::DeserializeTopology(const tinyxml2::XMLElement& topologyElement, + CAgentTopology& topology) +{ + // Deserialize ID + const char* id = topologyElement.Attribute(XML_ATTR_TOPOLOGY_ID); + if (id != nullptr) + { + int signedId = std::stoi(id); + if (signedId < 0) + { + CLog::Log(LOGERROR, "Invalid attribute \"{}\": \"{}\"", XML_ATTR_TOPOLOGY_ID, id); + return false; + } + + topology.SetID(static_cast(signedId)); + } + + // Deserialize digest + const char* digest = topologyElement.Attribute(XML_ATTR_SHA2_256); + if (digest != nullptr) + topology.SetDigest(digest); + + // Deserialize digest creation date + const char* digestCreation = topologyElement.Attribute(XML_ATTR_DIGTEST_CREATION); + if (digestCreation != nullptr) + { + CDateTime digestCreationUtc; + if (!digestCreationUtc.SetFromW3CDateTime(digestCreation, false)) + { + CLog::Log(LOGERROR, "Invalid attribute \"{}\": \"{}\"", XML_ATTR_DIGTEST_CREATION, + digestCreation); + return false; + } + topology.SetDigestCreationUTC(digestCreationUtc); + } + + // Get "definition" element + const tinyxml2::XMLElement* definitionElement = + topologyElement.FirstChildElement(XML_ELM_DEFINITION); + if (definitionElement == nullptr) + { + CLog::Log(LOGERROR, "Missing element <{}>", XML_ELM_DEFINITION); + return false; + } + + // Deserialize controller tree + if (!topology.DeserializeControllerTree(*definitionElement)) + return false; + + // Deserialize game clients + std::set gameClients; + if (!DeserializeGameClients(topologyElement, gameClients)) + return false; + + // Set game clients + topology.SetGameClients(std::move(gameClients)); + + return true; +} + +bool CAgentTopologyXML::DeserializeControllerTree(const CXBMCTinyXML2& xmlDoc, + CControllerTree& controllerTree) +{ + // Validate root element + const tinyxml2::XMLElement* topologyElement = xmlDoc.RootElement(); + if (topologyElement == nullptr || std::strcmp(topologyElement->Value(), XML_ELM_TOPOLOGY) != 0) + { + CLog::Log(LOGERROR, "Can't find root <{}> tag", XML_ELM_TOPOLOGY); + return false; + } + + return DeserializeControllerTree(*topologyElement, controllerTree); +} + +bool CAgentTopologyXML::DeserializeControllerTree(const tinyxml2::XMLElement& topologyElement, + CControllerTree& controllerTree) +{ + return controllerTree.Deserialize(topologyElement); +} + +bool CAgentTopologyXML::DeserializeGameClients(const tinyxml2::XMLElement& topologyElement, + std::set& gameClients) +{ + // Get "gameclients" element + const tinyxml2::XMLElement* gameClientsElement = + topologyElement.FirstChildElement(XML_ELM_GAMECLIENTS); + if (gameClientsElement == nullptr) + { + CLog::Log(LOGERROR, "Missing element <{}>", XML_ELM_GAMECLIENTS); + return false; + } + + // Get first "gameclient" element + const tinyxml2::XMLElement* gameClientElement = + gameClientsElement->FirstChildElement(XML_ELM_GAMECLIENT); + if (gameClientElement == nullptr) + { + CLog::Log(LOGERROR, "Missing element <{}>", XML_ELM_GAMECLIENT); + return false; + } + + // Deserialize game clients + while (gameClientElement != nullptr) + { + // Get ID of the game client + const char* gameClientId = gameClientElement->GetText(); + if (gameClientId == nullptr) + continue; + + // Add game client + gameClients.insert(gameClientId); + + // Get next "gameclient" element + gameClientElement = gameClientElement->NextSiblingElement(XML_ELM_GAMECLIENT); + } + + return true; +} diff --git a/xbmc/games/agents/input/AgentTopologyXML.h b/xbmc/games/agents/input/AgentTopologyXML.h new file mode 100644 index 0000000000000..63dbd035100f5 --- /dev/null +++ b/xbmc/games/agents/input/AgentTopologyXML.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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 "games/controllers/types/ControllerTree.h" +#include "utils/XBMCTinyXML2.h" + +#include +#include + +namespace tinyxml2 +{ +class XMLElement; +} + +namespace KODI +{ +namespace GAME +{ +class CAgentTopology; + +/*! + * \ingroup games + */ +class CAgentTopologyXML +{ +public: + static bool SerializeTopology(tinyxml2::XMLElement& topologyElement, + const CAgentTopology& topology); + + static bool DeserializeTopology(const tinyxml2::XMLElement& topologyElement, + CAgentTopology& topology); + static bool DeserializeControllerTree(const CXBMCTinyXML2& xmlDoc, + CControllerTree& controllerTree); + static bool DeserializeControllerTree(const tinyxml2::XMLElement& topologyElement, + CControllerTree& controllerTree); + static bool DeserializeGameClients(const tinyxml2::XMLElement& topologyElement, + std::set& gameClients); +}; +} // namespace GAME +} // namespace KODI diff --git a/xbmc/games/agents/input/CMakeLists.txt b/xbmc/games/agents/input/CMakeLists.txt index 545b9fad6a954..110c61747029b 100644 --- a/xbmc/games/agents/input/CMakeLists.txt +++ b/xbmc/games/agents/input/CMakeLists.txt @@ -1,15 +1,23 @@ set(SOURCES AgentController.cpp AgentInput.cpp + AgentInputMap.cpp + AgentInputMapXML.cpp AgentJoystick.cpp AgentKeyboard.cpp AgentMouse.cpp + AgentTopology.cpp + AgentTopologyXML.cpp ) set(HEADERS AgentController.h AgentInput.h + AgentInputMap.h + AgentInputMapXML.h AgentJoystick.h AgentKeyboard.h AgentMouse.h + AgentTopology.h + AgentTopologyXML.h ) core_add_library(games_agents_input)