Skip to content

Commit

Permalink
[Agents] Add agent input map
Browse files Browse the repository at this point in the history
  • Loading branch information
garbear committed Mar 9, 2024
1 parent 091e192 commit fb5909b
Show file tree
Hide file tree
Showing 14 changed files with 887 additions and 2 deletions.
2 changes: 2 additions & 0 deletions xbmc/ServiceManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ bool CServiceManager::InitStageThree(const std::shared_ptr<CProfileManager>& pro
m_gameServices = std::make_unique<GAME::CGameServices>(
*m_gameControllerManager, *m_gameRenderManager, *m_peripherals, *profileManager,
*m_inputManager, *m_addonMgr);
m_gameServices->Initialize();

m_contextMenuManager->Init();

Expand All @@ -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();

Expand Down
12 changes: 12 additions & 0 deletions xbmc/games/GameServices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions xbmc/games/GameServices.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
23 changes: 21 additions & 2 deletions xbmc/games/agents/input/AgentInput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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<CAgentInputMap>())
{
}

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)
Expand All @@ -53,6 +69,9 @@ void CAgentInput::Start(GameClientPtr gameClient)

// Perform initial refresh
Refresh();

// Update input map
m_inputMap->AddGameClient(m_gameClient);
}

void CAgentInput::Stop()
Expand Down
8 changes: 8 additions & 0 deletions xbmc/games/agents/input/AgentInput.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class IMouseInputProvider;

namespace GAME
{
class CAgentInputMap;
class CGameClient;
class CGameClientJoystick;

Expand Down Expand Up @@ -73,6 +74,8 @@ class CAgentInput : public Observable,
virtual ~CAgentInput();

// Lifecycle functions
void Initialize();
void Deinitialize();
void Start(GameClientPtr gameClient);
void Stop();
void Refresh();
Expand Down Expand Up @@ -204,6 +207,11 @@ class CAgentInput : public Observable,
* Source peripherals are not exposed to the game.
*/
std::set<PERIPHERALS::PeripheralPtr> m_disconnectedPeripherals;

/*!
* \brief Input map for the agents
*/
std::unique_ptr<CAgentInputMap> m_inputMap;
};
} // namespace GAME
} // namespace KODI
208 changes: 208 additions & 0 deletions xbmc/games/agents/input/AgentInputMap.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstring>

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<void>& 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<CAgentTopology> agentTopology = std::make_shared<CAgentTopology>();
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<std::string>& 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<void>& task) {
return task.wait_for(std::chrono::seconds(0)) ==
std::future_status::ready;
}),
m_saveFutures.end());

// Save async
std::future<void> 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<std::mutex> lock(m_saveMutex);
doc.SaveFile(m_topologiesXmlPath);
}
});

m_saveFutures.emplace_back(std::move(task));
}
Loading

0 comments on commit fb5909b

Please sign in to comment.