summaryrefslogtreecommitdiffstats
path: root/xbmc/games/ports
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/games/ports')
-rw-r--r--xbmc/games/ports/input/CMakeLists.txt11
-rw-r--r--xbmc/games/ports/input/PhysicalPort.cpp66
-rw-r--r--xbmc/games/ports/input/PhysicalPort.h64
-rw-r--r--xbmc/games/ports/input/PortInput.cpp129
-rw-r--r--xbmc/games/ports/input/PortInput.h77
-rw-r--r--xbmc/games/ports/input/PortManager.cpp347
-rw-r--r--xbmc/games/ports/input/PortManager.h81
-rw-r--r--xbmc/games/ports/types/CMakeLists.txt7
-rw-r--r--xbmc/games/ports/types/PortNode.cpp157
-rw-r--r--xbmc/games/ports/types/PortNode.h130
-rw-r--r--xbmc/games/ports/windows/CMakeLists.txt11
-rw-r--r--xbmc/games/ports/windows/GUIPortDefines.h22
-rw-r--r--xbmc/games/ports/windows/GUIPortList.cpp344
-rw-r--r--xbmc/games/ports/windows/GUIPortList.h79
-rw-r--r--xbmc/games/ports/windows/GUIPortWindow.cpp183
-rw-r--r--xbmc/games/ports/windows/GUIPortWindow.h56
-rw-r--r--xbmc/games/ports/windows/IPortList.h104
17 files changed, 1868 insertions, 0 deletions
diff --git a/xbmc/games/ports/input/CMakeLists.txt b/xbmc/games/ports/input/CMakeLists.txt
new file mode 100644
index 0000000..5b2b27f
--- /dev/null
+++ b/xbmc/games/ports/input/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES PhysicalPort.cpp
+ PortInput.cpp
+ PortManager.cpp
+)
+
+set(HEADERS PhysicalPort.h
+ PortInput.h
+ PortManager.h
+)
+
+core_add_library(games_ports_input)
diff --git a/xbmc/games/ports/input/PhysicalPort.cpp b/xbmc/games/ports/input/PhysicalPort.cpp
new file mode 100644
index 0000000..9c53c76
--- /dev/null
+++ b/xbmc/games/ports/input/PhysicalPort.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017-2021 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 "PhysicalPort.h"
+
+#include "games/controllers/ControllerDefinitions.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CPhysicalPort::CPhysicalPort(std::string portId, std::vector<std::string> accepts)
+ : m_portId(std::move(portId)), m_accepts(std::move(accepts))
+{
+}
+
+void CPhysicalPort::Reset()
+{
+ CPhysicalPort defaultPort;
+ *this = std::move(defaultPort);
+}
+
+bool CPhysicalPort::IsCompatible(const std::string& controllerId) const
+{
+ return std::find(m_accepts.begin(), m_accepts.end(), controllerId) != m_accepts.end();
+}
+
+bool CPhysicalPort::Deserialize(const TiXmlElement* pElement)
+{
+ if (pElement == nullptr)
+ return false;
+
+ Reset();
+
+ m_portId = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_PORT_ID);
+
+ for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr;
+ pChild = pChild->NextSiblingElement())
+ {
+ if (pChild->ValueStr() == LAYOUT_XML_ELM_ACCEPTS)
+ {
+ std::string controller = XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CONTROLLER);
+
+ if (!controller.empty())
+ m_accepts.emplace_back(std::move(controller));
+ else
+ CLog::Log(LOGWARNING, "<{}> tag is missing \"{}\" attribute", LAYOUT_XML_ELM_ACCEPTS,
+ LAYOUT_XML_ATTR_CONTROLLER);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Unknown physical topology port tag: <{}>", pChild->ValueStr());
+ }
+ }
+
+ return true;
+}
diff --git a/xbmc/games/ports/input/PhysicalPort.h b/xbmc/games/ports/input/PhysicalPort.h
new file mode 100644
index 0000000..83fe3a5
--- /dev/null
+++ b/xbmc/games/ports/input/PhysicalPort.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017-2021 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 <string>
+#include <vector>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CPhysicalPort
+{
+public:
+ CPhysicalPort() = default;
+
+ /*!
+ * \brief Create a controller port
+ *
+ * \param portId The port's ID
+ * \param accepts A list of controller IDs that this port accepts
+ */
+ CPhysicalPort(std::string portId, std::vector<std::string> accepts);
+
+ void Reset();
+
+ /*!
+ * \brief Get the ID of the port
+ *
+ * \return The port's ID, e.g. "1", as a string
+ */
+ const std::string& ID() const { return m_portId; }
+
+ /*!
+ * \brief Get the controllers that can connect to this port
+ *
+ * \return A list of controllers that are physically compatible with this port
+ */
+ const std::vector<std::string>& Accepts() const { return m_accepts; }
+
+ /*!
+ * \brief Check if the controller is compatible with this port
+ *
+ * \return True if the controller is accepted, false otherwise
+ */
+ bool IsCompatible(const std::string& controllerId) const;
+
+ bool Deserialize(const TiXmlElement* pElement);
+
+private:
+ std::string m_portId;
+ std::vector<std::string> m_accepts;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/input/PortInput.cpp b/xbmc/games/ports/input/PortInput.cpp
new file mode 100644
index 0000000..af1652b
--- /dev/null
+++ b/xbmc/games/ports/input/PortInput.cpp
@@ -0,0 +1,129 @@
+/*
+ * 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 "PortInput.h"
+
+#include "games/addons/GameClient.h"
+#include "games/controllers/input/InputSink.h"
+#include "guilib/WindowIDs.h"
+#include "input/joysticks/keymaps/KeymapHandling.h"
+#include "peripherals/devices/Peripheral.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CPortInput::CPortInput(JOYSTICK::IInputHandler* gameInput)
+ : m_gameInput(gameInput), m_inputSink(new CInputSink(gameInput))
+{
+}
+
+CPortInput::~CPortInput() = default;
+
+void CPortInput::RegisterInput(JOYSTICK::IInputProvider* provider)
+{
+ // Give input sink the lowest priority by registering it before the other
+ // input handlers
+ provider->RegisterInputHandler(m_inputSink.get(), false);
+
+ // Register input handler
+ provider->RegisterInputHandler(this, false);
+
+ // Register GUI input
+ m_appInput.reset(new JOYSTICK::CKeymapHandling(provider, false, this));
+}
+
+void CPortInput::UnregisterInput(JOYSTICK::IInputProvider* provider)
+{
+ // Unregister in reverse order
+ if (provider == nullptr && m_appInput)
+ m_appInput->UnregisterInputProvider();
+ m_appInput.reset();
+
+ if (provider != nullptr)
+ {
+ provider->UnregisterInputHandler(this);
+ provider->UnregisterInputHandler(m_inputSink.get());
+ }
+}
+
+std::string CPortInput::ControllerID() const
+{
+ return m_gameInput->ControllerID();
+}
+
+bool CPortInput::AcceptsInput(const std::string& feature) const
+{
+ return m_gameInput->AcceptsInput(feature);
+}
+
+bool CPortInput::OnButtonPress(const std::string& feature, bool bPressed)
+{
+ if (bPressed && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnButtonPress(feature, bPressed);
+}
+
+void CPortInput::OnButtonHold(const std::string& feature, unsigned int holdTimeMs)
+{
+ m_gameInput->OnButtonHold(feature, holdTimeMs);
+}
+
+bool CPortInput::OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ if (magnitude > 0.0f && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnButtonMotion(feature, magnitude, motionTimeMs);
+}
+
+bool CPortInput::OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ if ((x != 0.0f || y != 0.0f) && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnAnalogStickMotion(feature, x, y, motionTimeMs);
+}
+
+bool CPortInput::OnAccelerometerMotion(const std::string& feature, float x, float y, float z)
+{
+ if (!m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnAccelerometerMotion(feature, x, y, z);
+}
+
+bool CPortInput::OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ if ((position != 0.0f) && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnWheelMotion(feature, position, motionTimeMs);
+}
+
+bool CPortInput::OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ if ((position != 0.0f) && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnThrottleMotion(feature, position, motionTimeMs);
+}
+
+int CPortInput::GetWindowID() const
+{
+ return WINDOW_FULLSCREEN_GAME;
+}
diff --git a/xbmc/games/ports/input/PortInput.h b/xbmc/games/ports/input/PortInput.h
new file mode 100644
index 0000000..d3c6524
--- /dev/null
+++ b/xbmc/games/ports/input/PortInput.h
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "input/KeymapEnvironment.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CKeymapHandling;
+class IInputProvider;
+} // namespace JOYSTICK
+
+namespace GAME
+{
+class CPortInput : public JOYSTICK::IInputHandler, public IKeymapEnvironment
+{
+public:
+ CPortInput(JOYSTICK::IInputHandler* gameInput);
+ ~CPortInput() override;
+
+ void RegisterInput(JOYSTICK::IInputProvider* provider);
+ void UnregisterInput(JOYSTICK::IInputProvider* provider);
+
+ JOYSTICK::IInputHandler* InputHandler() { return m_gameInput; }
+
+ // Implementation of IInputHandler
+ std::string ControllerID() const override;
+ bool HasFeature(const std::string& feature) const override { return true; }
+ bool AcceptsInput(const std::string& feature) const override;
+ bool OnButtonPress(const std::string& feature, bool bPressed) override;
+ void OnButtonHold(const std::string& feature, unsigned int holdTimeMs) override;
+ bool OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs) override;
+ bool OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs) override;
+ bool OnAccelerometerMotion(const std::string& feature, float x, float y, float z) override;
+ bool OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ bool OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ void OnInputFrame() override {}
+
+ // Implementation of IKeymapEnvironment
+ int GetWindowID() const override;
+ void SetWindowID(int windowId) override {}
+ int GetFallthrough(int windowId) const override { return -1; }
+ bool UseGlobalFallthrough() const override { return false; }
+ bool UseEasterEgg() const override { return false; }
+
+private:
+ // Construction parameters
+ JOYSTICK::IInputHandler* const m_gameInput;
+
+ // Handles input to Kodi
+ std::unique_ptr<JOYSTICK::CKeymapHandling> m_appInput;
+
+ // Prevents input falling through to Kodi when not handled by the game
+ std::unique_ptr<JOYSTICK::IInputHandler> m_inputSink;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/input/PortManager.cpp b/xbmc/games/ports/input/PortManager.cpp
new file mode 100644
index 0000000..a04177a
--- /dev/null
+++ b/xbmc/games/ports/input/PortManager.cpp
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2021 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 "PortManager.h"
+
+#include "URL.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/controllers/types/ControllerNode.h"
+#include "games/ports/types/PortNode.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+namespace
+{
+constexpr const char* PORT_XML_FILE = "ports.xml";
+constexpr const char* XML_ROOT_PORTS = "ports";
+constexpr const char* XML_ELM_PORT = "port";
+constexpr const char* XML_ELM_CONTROLLER = "controller";
+constexpr const char* XML_ATTR_PORT_ID = "id";
+constexpr const char* XML_ATTR_PORT_ADDRESS = "address";
+constexpr const char* XML_ATTR_PORT_CONNECTED = "connected";
+constexpr const char* XML_ATTR_PORT_CONTROLLER = "controller";
+constexpr const char* XML_ATTR_CONTROLLER_ID = "id";
+} // namespace
+
+CPortManager::CPortManager() = default;
+
+CPortManager::~CPortManager() = default;
+
+void CPortManager::Initialize(const std::string& profilePath)
+{
+ m_xmlPath = URIUtils::AddFileToFolder(profilePath, PORT_XML_FILE);
+}
+
+void CPortManager::Deinitialize()
+{
+ // Wait for save tasks
+ for (std::future<void>& task : m_saveFutures)
+ task.wait();
+ m_saveFutures.clear();
+
+ m_controllerTree.Clear();
+ m_xmlPath.clear();
+}
+
+void CPortManager::SetControllerTree(const CControllerTree& controllerTree)
+{
+ m_controllerTree = controllerTree;
+}
+
+void CPortManager::LoadXML()
+{
+ if (!CFileUtils::Exists(m_xmlPath))
+ {
+ CLog::Log(LOGDEBUG, "Can't load port config, file doesn't exist: {}", m_xmlPath);
+ return;
+ }
+
+ CLog::Log(LOGINFO, "Loading port layout: {}", CURL::GetRedacted(m_xmlPath));
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(m_xmlPath))
+ {
+ CLog::Log(LOGDEBUG, "Unable to load file: {} at line {}", xmlDoc.ErrorDesc(),
+ xmlDoc.ErrorRow());
+ return;
+ }
+
+ const TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (pRootElement == nullptr || pRootElement->NoChildren() ||
+ pRootElement->ValueStr() != XML_ROOT_PORTS)
+ {
+ CLog::Log(LOGERROR, "Can't find root <{}> tag", XML_ROOT_PORTS);
+ return;
+ }
+
+ DeserializePorts(pRootElement, m_controllerTree.GetPorts());
+}
+
+void CPortManager::SaveXMLAsync()
+{
+ PortVec ports = m_controllerTree.GetPorts();
+
+ // 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, ports = std::move(ports)]() {
+ CXBMCTinyXML doc;
+ TiXmlElement node(XML_ROOT_PORTS);
+
+ SerializePorts(node, ports);
+
+ doc.InsertEndChild(node);
+
+ std::lock_guard<std::mutex> lock(m_saveMutex);
+ doc.SaveFile(m_xmlPath);
+ });
+
+ m_saveFutures.emplace_back(std::move(task));
+}
+
+void CPortManager::Clear()
+{
+ m_xmlPath.clear();
+ m_controllerTree.Clear();
+}
+
+void CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId /* = "" */)
+{
+ ConnectController(portAddress, connected, controllerId, m_controllerTree.GetPorts());
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ PortVec& ports)
+{
+ for (CPortNode& port : ports)
+ {
+ if (ConnectController(portAddress, connected, controllerId, port))
+ return true;
+ }
+
+ return false;
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CPortNode& port)
+{
+ // Base case
+ if (port.GetAddress() == portAddress)
+ {
+ port.SetConnected(connected);
+ if (!controllerId.empty())
+ port.SetActiveController(controllerId);
+ return true;
+ }
+
+ // Check children
+ return ConnectController(portAddress, connected, controllerId, port.GetCompatibleControllers());
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ ControllerNodeVec& controllers)
+{
+ for (CControllerNode& controller : controllers)
+ {
+ if (ConnectController(portAddress, connected, controllerId, controller))
+ return true;
+ }
+
+ return false;
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CControllerNode& controller)
+{
+ for (CPortNode& childPort : controller.GetHub().GetPorts())
+ {
+ if (ConnectController(portAddress, connected, controllerId, childPort))
+ return true;
+ }
+
+ return false;
+}
+
+void CPortManager::DeserializePorts(const TiXmlElement* pElement, PortVec& ports)
+{
+ for (const TiXmlElement* pPort = pElement->FirstChildElement(); pPort != nullptr;
+ pPort = pPort->NextSiblingElement())
+ {
+ if (pPort->ValueStr() != XML_ELM_PORT)
+ {
+ CLog::Log(LOGDEBUG, "Inside <{}> tag: Ignoring <{}> tag", pElement->ValueStr(),
+ pPort->ValueStr());
+ continue;
+ }
+
+ std::string portId = XMLUtils::GetAttribute(pPort, XML_ATTR_PORT_ID);
+
+ auto it = std::find_if(ports.begin(), ports.end(),
+ [&portId](const CPortNode& port) { return port.GetPortID() == portId; });
+ if (it != ports.end())
+ {
+ CPortNode& port = *it;
+
+ DeserializePort(pPort, port);
+ }
+ }
+}
+
+void CPortManager::DeserializePort(const TiXmlElement* pPort, CPortNode& port)
+{
+ // Connected
+ bool connected = (XMLUtils::GetAttribute(pPort, XML_ATTR_PORT_CONNECTED) == "true");
+ port.SetConnected(connected);
+
+ // Controller
+ const std::string activeControllerId = XMLUtils::GetAttribute(pPort, XML_ATTR_PORT_CONTROLLER);
+ if (!port.SetActiveController(activeControllerId))
+ port.SetConnected(false);
+
+ DeserializeControllers(pPort, port.GetCompatibleControllers());
+}
+
+void CPortManager::DeserializeControllers(const TiXmlElement* pPort, ControllerNodeVec& controllers)
+{
+ for (const TiXmlElement* pController = pPort->FirstChildElement(); pController != nullptr;
+ pController = pController->NextSiblingElement())
+ {
+ if (pController->ValueStr() != XML_ELM_CONTROLLER)
+ {
+ CLog::Log(LOGDEBUG, "Inside <{}> tag: Ignoring <{}> tag", pPort->ValueStr(),
+ pController->ValueStr());
+ continue;
+ }
+
+ std::string controllerId = XMLUtils::GetAttribute(pController, XML_ATTR_CONTROLLER_ID);
+
+ auto it = std::find_if(controllers.begin(), controllers.end(),
+ [&controllerId](const CControllerNode& controller) {
+ return controller.GetController()->ID() == controllerId;
+ });
+ if (it != controllers.end())
+ {
+ CControllerNode& controller = *it;
+
+ DeserializeController(pController, controller);
+ }
+ }
+}
+
+void CPortManager::DeserializeController(const TiXmlElement* pController,
+ CControllerNode& controller)
+{
+ // Child ports
+ DeserializePorts(pController, controller.GetHub().GetPorts());
+}
+
+void CPortManager::SerializePorts(TiXmlElement& node, const PortVec& ports)
+{
+ for (const CPortNode& port : ports)
+ {
+ TiXmlElement portNode(XML_ELM_PORT);
+
+ SerializePort(portNode, port);
+
+ node.InsertEndChild(portNode);
+ }
+}
+
+void CPortManager::SerializePort(TiXmlElement& portNode, const CPortNode& port)
+{
+ // Port ID
+ portNode.SetAttribute(XML_ATTR_PORT_ID, port.GetPortID());
+
+ // Port address
+ portNode.SetAttribute(XML_ATTR_PORT_ADDRESS, port.GetAddress());
+
+ // Connected state
+ portNode.SetAttribute(XML_ATTR_PORT_CONNECTED, port.IsConnected() ? "true" : "false");
+
+ // Active controller
+ if (port.GetActiveController().GetController())
+ {
+ const std::string controllerId = port.GetActiveController().GetController()->ID();
+ portNode.SetAttribute(XML_ATTR_PORT_CONTROLLER, controllerId);
+ }
+
+ // All compatible controllers
+ SerializeControllers(portNode, port.GetCompatibleControllers());
+}
+
+void CPortManager::SerializeControllers(TiXmlElement& portNode,
+ const ControllerNodeVec& controllers)
+{
+ for (const CControllerNode& controller : controllers)
+ {
+ // Skip controller if it has no state
+ if (!HasState(controller))
+ continue;
+
+ TiXmlElement controllerNode(XML_ELM_CONTROLLER);
+
+ SerializeController(controllerNode, controller);
+
+ portNode.InsertEndChild(controllerNode);
+ }
+}
+
+void CPortManager::SerializeController(TiXmlElement& controllerNode,
+ const CControllerNode& controller)
+{
+ // Controller ID
+ if (controller.GetController())
+ controllerNode.SetAttribute(XML_ATTR_CONTROLLER_ID, controller.GetController()->ID());
+
+ // Ports
+ SerializePorts(controllerNode, controller.GetHub().GetPorts());
+}
+
+bool CPortManager::HasState(const CPortNode& port)
+{
+ // Ports have state (is connected / active controller)
+ return true;
+}
+
+bool CPortManager::HasState(const CControllerNode& controller)
+{
+ // Check controller ports
+ for (const CPortNode& port : controller.GetHub().GetPorts())
+ {
+ if (HasState(port))
+ return true;
+ }
+
+ // Controller itself has no state
+ return false;
+}
diff --git a/xbmc/games/ports/input/PortManager.h b/xbmc/games/ports/input/PortManager.h
new file mode 100644
index 0000000..a02ad35
--- /dev/null
+++ b/xbmc/games/ports/input/PortManager.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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 <future>
+#include <mutex>
+#include <string>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPortManager
+{
+public:
+ CPortManager();
+ ~CPortManager();
+
+ void Initialize(const std::string& profilePath);
+ void Deinitialize();
+
+ void SetControllerTree(const CControllerTree& controllerTree);
+ void LoadXML();
+ void SaveXMLAsync();
+ void Clear();
+
+ void ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId = "");
+
+ const CControllerTree& GetControllerTree() const { return m_controllerTree; }
+
+private:
+ static void DeserializePorts(const TiXmlElement* pElement, PortVec& ports);
+ static void DeserializePort(const TiXmlElement* pElement, CPortNode& port);
+ static void DeserializeControllers(const TiXmlElement* pElement, ControllerNodeVec& controllers);
+ static void DeserializeController(const TiXmlElement* pElement, CControllerNode& controller);
+
+ static void SerializePorts(TiXmlElement& node, const PortVec& ports);
+ static void SerializePort(TiXmlElement& portNode, const CPortNode& port);
+ static void SerializeControllers(TiXmlElement& portNode, const ControllerNodeVec& controllers);
+ static void SerializeController(TiXmlElement& controllerNode, const CControllerNode& controller);
+
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ PortVec& ports);
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CPortNode& port);
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ ControllerNodeVec& controllers);
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CControllerNode& controller);
+
+ static bool HasState(const CPortNode& port);
+ static bool HasState(const CControllerNode& controller);
+
+ CControllerTree m_controllerTree;
+ std::string m_xmlPath;
+
+ std::vector<std::future<void>> m_saveFutures;
+ std::mutex m_saveMutex;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/types/CMakeLists.txt b/xbmc/games/ports/types/CMakeLists.txt
new file mode 100644
index 0000000..735df42
--- /dev/null
+++ b/xbmc/games/ports/types/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES PortNode.cpp
+)
+
+set(HEADERS PortNode.h
+)
+
+core_add_library(games_ports_types)
diff --git a/xbmc/games/ports/types/PortNode.cpp b/xbmc/games/ports/types/PortNode.cpp
new file mode 100644
index 0000000..7caf6eb
--- /dev/null
+++ b/xbmc/games/ports/types/PortNode.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017-2021 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 "PortNode.h"
+
+#include "games/controllers/Controller.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/ports/input/PhysicalPort.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CPortNode::~CPortNode() = default;
+
+CPortNode& CPortNode::operator=(const CPortNode& rhs)
+{
+ if (this != &rhs)
+ {
+ m_bConnected = rhs.m_bConnected;
+ m_active = rhs.m_active;
+ m_portType = rhs.m_portType;
+ m_portId = rhs.m_portId;
+ m_address = rhs.m_address;
+ m_forceConnected = rhs.m_forceConnected;
+ m_controllers = rhs.m_controllers;
+ }
+
+ return *this;
+}
+
+CPortNode& CPortNode::operator=(CPortNode&& rhs) noexcept
+{
+ if (this != &rhs)
+ {
+ m_bConnected = rhs.m_bConnected;
+ m_active = rhs.m_active;
+ m_portType = rhs.m_portType;
+ m_portId = std::move(rhs.m_portId);
+ m_address = std::move(rhs.m_address);
+ m_forceConnected = rhs.m_forceConnected;
+ m_controllers = std::move(rhs.m_controllers);
+ }
+
+ return *this;
+}
+
+const CControllerNode& CPortNode::GetActiveController() const
+{
+ if (m_bConnected && m_active < m_controllers.size())
+ return m_controllers[m_active];
+
+ static const CControllerNode invalid{};
+ return invalid;
+}
+
+CControllerNode& CPortNode::GetActiveController()
+{
+ if (m_bConnected && m_active < m_controllers.size())
+ return m_controllers[m_active];
+
+ static CControllerNode invalid;
+ invalid.Clear();
+ return invalid;
+}
+
+bool CPortNode::SetActiveController(const std::string& controllerId)
+{
+ for (size_t i = 0; i < m_controllers.size(); ++i)
+ {
+ const ControllerPtr& controller = m_controllers.at(i).GetController();
+ if (controller && controller->ID() == controllerId)
+ {
+ m_active = i;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPortNode::SetPortID(std::string portId)
+{
+ m_portId = std::move(portId);
+}
+
+void CPortNode::SetAddress(std::string address)
+{
+ m_address = std::move(address);
+}
+
+void CPortNode::SetCompatibleControllers(ControllerNodeVec controllers)
+{
+ m_controllers = std::move(controllers);
+}
+
+bool CPortNode::IsControllerAccepted(const std::string& controllerId) const
+{
+ // Base case
+ CPhysicalPort port;
+ GetPort(port);
+ if (port.IsCompatible(controllerId))
+ return true;
+
+ // Visit nodes
+ return std::any_of(m_controllers.begin(), m_controllers.end(),
+ [controllerId](const CControllerNode& node) {
+ return node.IsControllerAccepted(controllerId);
+ });
+}
+
+bool CPortNode::IsControllerAccepted(const std::string& portAddress,
+ const std::string& controllerId) const
+{
+ bool bAccepted = false;
+
+ if (m_address == portAddress)
+ {
+ // Base case
+ CPhysicalPort port;
+ GetPort(port);
+ if (port.IsCompatible(controllerId))
+ bAccepted = true;
+ }
+ else
+ {
+ // Visit nodes
+ if (std::any_of(m_controllers.begin(), m_controllers.end(),
+ [portAddress, controllerId](const CControllerNode& node) {
+ return node.IsControllerAccepted(portAddress, controllerId);
+ }))
+ {
+ bAccepted = true;
+ }
+ }
+
+ return bAccepted;
+}
+
+void CPortNode::GetPort(CPhysicalPort& port) const
+{
+ std::vector<std::string> accepts;
+ for (const CControllerNode& node : m_controllers)
+ {
+ if (node.GetController())
+ accepts.emplace_back(node.GetController()->ID());
+ }
+
+ port = CPhysicalPort(m_portId, std::move(accepts));
+}
diff --git a/xbmc/games/ports/types/PortNode.h b/xbmc/games/ports/types/PortNode.h
new file mode 100644
index 0000000..660a7bc
--- /dev/null
+++ b/xbmc/games/ports/types/PortNode.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017-2021 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/ControllerTypes.h"
+#include "games/controllers/types/ControllerNode.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalPort;
+
+/*!
+ * \brief Collection of nodes that can be connected to this port
+ */
+class CPortNode
+{
+public:
+ CPortNode() = default;
+ CPortNode(const CPortNode& other) { *this = other; }
+ CPortNode(CPortNode&& other) = default;
+ CPortNode& operator=(const CPortNode& rhs);
+ CPortNode& operator=(CPortNode&& rhs) noexcept;
+ ~CPortNode();
+
+ /*!
+ * \brief Connection state of the port
+ *
+ * \return True if a controller is connected, false otherwise
+ */
+ bool IsConnected() const { return m_bConnected; }
+ void SetConnected(bool bConnected) { m_bConnected = bConnected; }
+
+ /*!
+ * \brief The controller that is active on this port
+ *
+ * \return The active controller, or invalid if port is disconnected
+ */
+ const CControllerNode& GetActiveController() const;
+ CControllerNode& GetActiveController();
+ void SetActiveController(unsigned int controllerIndex) { m_active = controllerIndex; }
+ bool SetActiveController(const std::string& controllerId);
+
+ /*!
+ * \brief The port type
+ *
+ * \return The port type, if known
+ */
+ PORT_TYPE GetPortType() const { return m_portType; }
+ void SetPortType(PORT_TYPE type) { m_portType = type; }
+
+ /*!
+ * \brief The hardware or controller port ID
+ *
+ * \return The port ID of the hardware port or controller port, or empty if
+ * the port is only identified by its type
+ */
+ const std::string& GetPortID() const { return m_portId; }
+ void SetPortID(std::string portId);
+
+ /*!
+ * \brief Address given to the node by the implementation
+ */
+ const std::string& GetAddress() const { return m_address; }
+ void SetAddress(std::string address);
+
+ /*!
+ * \brief If true, prevents a disconnection option from being shown for this
+ * port
+ */
+ bool IsForceConnected() const { return m_forceConnected; }
+ void SetForceConnected(bool forceConnected) { m_forceConnected = forceConnected; }
+
+ /*!
+ * \brief Return the controller profiles that are compatible with this port
+ *
+ * \return The controller profiles, or empty if this port doesn't support
+ * any controller profiles
+ */
+ const ControllerNodeVec& GetCompatibleControllers() const { return m_controllers; }
+ ControllerNodeVec& GetCompatibleControllers() { return m_controllers; }
+ void SetCompatibleControllers(ControllerNodeVec controllers);
+
+ /*!
+ * \brief Check to see if a controller is compatible with this tree
+ *
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with the tree, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& controllerId) const;
+
+ /*!
+ * \brief Check to see if a controller is compatible with this tree
+ *
+ * \param portAddress The port address
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with the tree, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const;
+
+private:
+ void GetPort(CPhysicalPort& port) const;
+
+ bool m_bConnected = false;
+ unsigned int m_active = 0;
+ PORT_TYPE m_portType = PORT_TYPE::UNKNOWN;
+ std::string m_portId;
+ std::string m_address;
+ bool m_forceConnected{true};
+ ControllerNodeVec m_controllers;
+};
+
+/*!
+ * \brief Collection of port nodes
+ */
+using PortVec = std::vector<CPortNode>;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/windows/CMakeLists.txt b/xbmc/games/ports/windows/CMakeLists.txt
new file mode 100644
index 0000000..4cf07ea
--- /dev/null
+++ b/xbmc/games/ports/windows/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES GUIPortList.cpp
+ GUIPortWindow.cpp
+)
+
+set(HEADERS GUIPortDefines.h
+ GUIPortList.h
+ GUIPortWindow.h
+ IPortList.h
+)
+
+core_add_library(games_ports_windows)
diff --git a/xbmc/games/ports/windows/GUIPortDefines.h b/xbmc/games/ports/windows/GUIPortDefines.h
new file mode 100644
index 0000000..c9c255b
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortDefines.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 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
+
+// Dialog title
+#define CONTROL_PORT_DIALOG_LABEL 2
+
+// GUI control IDs
+#define CONTROL_PORT_LIST 3
+
+// GUI button IDs
+#define CONTROL_CLOSE_BUTTON 18
+#define CONTROL_RESET_BUTTON 19
+
+// Skin XML file
+#define PORT_DIALOG_XML "DialogGameControllers.xml"
diff --git a/xbmc/games/ports/windows/GUIPortList.cpp b/xbmc/games/ports/windows/GUIPortList.cpp
new file mode 100644
index 0000000..a25c94b
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortList.cpp
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2021 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 "GUIPortList.h"
+
+#include "FileItem.h"
+#include "GUIPortDefines.h"
+#include "GUIPortWindow.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "games/GameServices.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "games/ports/types/PortNode.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "view/GUIViewControl.h"
+#include "view/ViewState.h"
+
+using namespace KODI;
+using namespace ADDON;
+using namespace GAME;
+
+CGUIPortList::CGUIPortList(CGUIWindow& window)
+ : m_guiWindow(window),
+ m_viewControl(std::make_unique<CGUIViewControl>()),
+ m_vecItems(std::make_unique<CFileItemList>())
+{
+}
+
+CGUIPortList::~CGUIPortList()
+{
+ Deinitialize();
+}
+
+void CGUIPortList::OnWindowLoaded()
+{
+ m_viewControl->Reset();
+ m_viewControl->SetParentWindow(m_guiWindow.GetID());
+ m_viewControl->AddView(m_guiWindow.GetControl(CONTROL_PORT_LIST));
+}
+
+void CGUIPortList::OnWindowUnload()
+{
+ m_viewControl->Reset();
+}
+
+bool CGUIPortList::Initialize(GameClientPtr gameClient)
+{
+ // Validate parameters
+ if (!gameClient)
+ return false;
+
+ // Initialize state
+ m_gameClient = std::move(gameClient);
+ m_viewControl->SetCurrentView(DEFAULT_VIEW_LIST);
+
+ // Initialize GUI
+ Refresh();
+
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIPortList::OnEvent);
+
+ return true;
+}
+
+void CGUIPortList::Deinitialize()
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ // Deinitialize GUI
+ CleanupItems();
+
+ // Reset state
+ m_gameClient.reset();
+}
+
+bool CGUIPortList::HasControl(int controlId)
+{
+ return m_viewControl->HasControl(controlId);
+}
+
+int CGUIPortList::GetCurrentControl()
+{
+ return m_viewControl->GetCurrentControl();
+}
+
+void CGUIPortList::Refresh()
+{
+ // Send a synchronous message to clear the view control
+ m_viewControl->Clear();
+
+ CleanupItems();
+
+ if (m_gameClient)
+ {
+ unsigned int itemIndex = 0;
+ for (const CPortNode& port : m_gameClient->Input().GetActiveControllerTree().GetPorts())
+ AddItems(port, itemIndex, GetLabel(port));
+
+ m_viewControl->SetItems(*m_vecItems);
+
+ // Try to restore focus to the previously focused port
+ if (!m_focusedPort.empty() && m_addressToItem.find(m_focusedPort) != m_addressToItem.end())
+ {
+ const unsigned int itemIndex = m_addressToItem[m_focusedPort];
+ m_viewControl->SetSelectedItem(itemIndex);
+ OnItemFocus(itemIndex);
+ }
+ }
+}
+
+void CGUIPortList::FrameMove()
+{
+ const int itemIndex = m_viewControl->GetSelectedItem();
+ if (itemIndex != m_currentItem)
+ {
+ m_currentItem = itemIndex;
+ if (itemIndex >= 0)
+ OnItemFocus(static_cast<unsigned int>(itemIndex));
+ }
+}
+
+void CGUIPortList::SetFocused()
+{
+ m_viewControl->SetFocused();
+}
+
+bool CGUIPortList::OnSelect()
+{
+ const int itemIndex = m_viewControl->GetSelectedItem();
+ if (itemIndex >= 0)
+ {
+ OnItemSelect(static_cast<unsigned int>(itemIndex));
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIPortList::ResetPorts()
+{
+ if (m_gameClient)
+ {
+ // Update the game client
+ m_gameClient->Input().ResetPorts();
+ m_gameClient->Input().SavePorts();
+
+ // Refresh the GUI
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow.GetID(), CONTROL_PORT_LIST);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow.GetID());
+ }
+}
+
+void CGUIPortList::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || // Also called on install
+ typeid(event) == typeid(ADDON::AddonEvents::Disabled) || // Not called on uninstall
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::UnInstalled))
+ {
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow.GetID(), CONTROL_PORT_LIST);
+ msg.SetStringParam(event.addonId);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow.GetID());
+ }
+}
+
+bool CGUIPortList::AddItems(const CPortNode& port,
+ unsigned int& itemId,
+ const std::string& itemLabel)
+{
+ // Validate parameters
+ if (itemLabel.empty())
+ return false;
+
+ // Record the port address so that we can decode item indexes later
+ m_itemToAddress[itemId] = port.GetAddress();
+ m_addressToItem[port.GetAddress()] = itemId;
+
+ if (port.IsConnected())
+ {
+ const CControllerNode& controllerNode = port.GetActiveController();
+ const ControllerPtr& controller = controllerNode.GetController();
+
+ // Create the list item
+ CFileItemPtr item = std::make_shared<CFileItem>(itemLabel);
+ item->SetLabel2(controller->Layout().Label());
+ item->SetPath(port.GetAddress());
+ item->SetArt("icon", controller->Layout().ImagePath());
+ m_vecItems->Add(std::move(item));
+ ++itemId;
+
+ // Handle items for child ports
+ const PortVec& ports = controllerNode.GetHub().GetPorts();
+ for (const CPortNode& childPort : ports)
+ {
+ std::ostringstream childItemLabel;
+ childItemLabel << " - ";
+ childItemLabel << controller->Layout().Label();
+ childItemLabel << " - ";
+ childItemLabel << GetLabel(childPort);
+
+ if (!AddItems(childPort, itemId, childItemLabel.str()))
+ return false;
+ }
+ }
+ else
+ {
+ // Create the list item
+ CFileItemPtr item = std::make_shared<CFileItem>(itemLabel);
+ item->SetLabel2(g_localizeStrings.Get(13298)); // "Disconnected"
+ item->SetPath(port.GetAddress());
+ item->SetArt("icon", "DefaultAddonNone.png");
+ m_vecItems->Add(std::move(item));
+ ++itemId;
+ }
+
+ return true;
+}
+
+void CGUIPortList::CleanupItems()
+{
+ m_vecItems->Clear();
+ m_itemToAddress.clear();
+ m_addressToItem.clear();
+}
+
+void CGUIPortList::OnItemFocus(unsigned int itemIndex)
+{
+ m_focusedPort = m_itemToAddress[itemIndex];
+}
+
+void CGUIPortList::OnItemSelect(unsigned int itemIndex)
+{
+ if (m_gameClient)
+ {
+ const auto it = m_itemToAddress.find(itemIndex);
+ if (it == m_itemToAddress.end())
+ return;
+
+ const std::string& portAddress = it->second;
+ if (portAddress.empty())
+ return;
+
+ const CPortNode& port = m_gameClient->Input().GetActiveControllerTree().GetPort(portAddress);
+
+ ControllerVector controllers;
+ for (const CControllerNode& controllerNode : port.GetCompatibleControllers())
+ controllers.emplace_back(controllerNode.GetController());
+
+ // Get current controller to give initial focus
+ ControllerPtr controller = port.GetActiveController().GetController();
+
+ auto callback = [this, &port](const ControllerPtr& controller) {
+ OnControllerSelected(port, controller);
+ };
+
+ const bool showDisconnect = !port.IsForceConnected();
+ m_controllerSelectDialog.Initialize(std::move(controllers), std::move(controller),
+ showDisconnect, callback);
+ }
+}
+
+void CGUIPortList::OnControllerSelected(const CPortNode& port, const ControllerPtr& controller)
+{
+ if (m_gameClient)
+ {
+ // Translate parameter
+ const bool bConnected = static_cast<bool>(controller);
+
+ // Update the game client
+ const bool bSuccess =
+ bConnected ? m_gameClient->Input().ConnectController(port.GetAddress(), controller)
+ : m_gameClient->Input().DisconnectController(port.GetAddress());
+
+ if (bSuccess)
+ {
+ m_gameClient->Input().SavePorts();
+ }
+ else
+ {
+ // "Failed to change controller"
+ // "The emulator "%s" had an internal error."
+ MESSAGING::HELPERS::ShowOKDialogText(
+ CVariant{35114},
+ CVariant{StringUtils::Format(g_localizeStrings.Get(35213), m_gameClient->Name())});
+ }
+
+ // Send a GUI message to reload the port list
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow.GetID(), CONTROL_PORT_LIST);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow.GetID());
+ }
+}
+
+std::string CGUIPortList::GetLabel(const CPortNode& port)
+{
+ const PORT_TYPE portType = port.GetPortType();
+ switch (portType)
+ {
+ case PORT_TYPE::KEYBOARD:
+ {
+ // "Keyboard"
+ return g_localizeStrings.Get(35150);
+ }
+ case PORT_TYPE::MOUSE:
+ {
+ // "Mouse"
+ return g_localizeStrings.Get(35171);
+ }
+ case PORT_TYPE::CONTROLLER:
+ {
+ const std::string& portId = port.GetPortID();
+ if (portId.empty())
+ {
+ CLog::Log(LOGERROR, "Controller port with address \"{}\" doesn't have a port ID",
+ port.GetAddress());
+ }
+ else
+ {
+ // "Port {0:s}"
+ const std::string& portString = g_localizeStrings.Get(35112);
+ return StringUtils::Format(portString, portId);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/games/ports/windows/GUIPortList.h b/xbmc/games/ports/windows/GUIPortList.h
new file mode 100644
index 0000000..3fed6b7
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortList.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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 "IPortList.h"
+#include "addons/AddonEvents.h"
+#include "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+#include "games/controllers/dialogs/ControllerSelect.h"
+#include "games/controllers/types/ControllerTree.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+class CFileItemList;
+class CGUIViewControl;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIPortList : public IPortList
+{
+public:
+ CGUIPortList(CGUIWindow& window);
+ ~CGUIPortList() override;
+
+ // Implementation of IPortList
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool Initialize(GameClientPtr gameClient) override;
+ void Deinitialize() override;
+ bool HasControl(int controlId) override;
+ int GetCurrentControl() override;
+ void Refresh() override;
+ void FrameMove() override;
+ void SetFocused() override;
+ bool OnSelect() override;
+ void ResetPorts() override;
+
+private:
+ // Add-on API
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ bool AddItems(const CPortNode& port, unsigned int& itemId, const std::string& itemLabel);
+ void CleanupItems();
+ void OnItemFocus(unsigned int itemIndex);
+ void OnItemSelect(unsigned int itemIndex);
+
+ // Controller selection callback
+ void OnControllerSelected(const CPortNode& port, const ControllerPtr& controller);
+
+ static std::string GetLabel(const CPortNode& port);
+
+ // Construction parameters
+ CGUIWindow& m_guiWindow;
+
+ // GUI parameters
+ CControllerSelect m_controllerSelectDialog;
+ std::string m_focusedPort; // Address of focused port
+ int m_currentItem{-1}; // Index of the selected item, or -1 if no item is selected
+ std::unique_ptr<CGUIViewControl> m_viewControl;
+ std::unique_ptr<CFileItemList> m_vecItems;
+
+ // Game parameters
+ GameClientPtr m_gameClient;
+ std::map<unsigned int, std::string> m_itemToAddress; // item index -> port address
+ std::map<std::string, unsigned int> m_addressToItem; // port address -> item index
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/windows/GUIPortWindow.cpp b/xbmc/games/ports/windows/GUIPortWindow.cpp
new file mode 100644
index 0000000..e95927a
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortWindow.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2021 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 "GUIPortWindow.h"
+
+#include "GUIPortDefines.h"
+#include "GUIPortList.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "games/addons/GameClient.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/ActionIDs.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIPortWindow::CGUIPortWindow()
+ : CGUIDialog(WINDOW_DIALOG_GAME_PORTS, PORT_DIALOG_XML),
+ m_portList(std::make_unique<CGUIPortList>(*this))
+{
+ // Initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIPortWindow::~CGUIPortWindow() = default;
+
+bool CGUIPortWindow::OnMessage(CGUIMessage& message)
+{
+ // Set to true to block the call to the super class
+ bool bHandled = false;
+
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_SETFOCUS:
+ {
+ const int controlId = message.GetControlId();
+ if (m_portList->HasControl(controlId) && m_portList->GetCurrentControl() != controlId)
+ {
+ FocusPortList();
+ bHandled = true;
+ }
+ break;
+ }
+ case GUI_MSG_CLICKED:
+ {
+ const int controlId = message.GetSenderId();
+
+ if (controlId == CONTROL_CLOSE_BUTTON)
+ {
+ CloseDialog();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_RESET_BUTTON)
+ {
+ ResetPorts();
+ bHandled = true;
+ }
+ else if (m_portList->HasControl(controlId))
+ {
+ const int actionId = message.GetParam1();
+ if (actionId == ACTION_SELECT_ITEM || actionId == ACTION_MOUSE_LEFT_CLICK)
+ {
+ OnClickAction();
+ bHandled = true;
+ }
+ }
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ UpdatePortList();
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!bHandled)
+ bHandled = CGUIDialog::OnMessage(message);
+
+ return bHandled;
+}
+
+void CGUIPortWindow::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_portList->OnWindowLoaded();
+}
+
+void CGUIPortWindow::OnWindowUnload()
+{
+ m_portList->OnWindowUnload();
+
+ CGUIDialog::OnWindowUnload();
+}
+
+void CGUIPortWindow::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ // Get active game add-on
+ GameClientPtr gameClient;
+ {
+ auto gameSettingsHandle = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+ if (gameSettingsHandle)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(gameSettingsHandle->GameClientID(), addon,
+ ADDON::AddonType::GAMEDLL,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ gameClient = std::static_pointer_cast<CGameClient>(addon);
+ }
+ }
+ m_gameClient = std::move(gameClient);
+
+ // Set the heading
+ // "Port Setup - {game client name}"
+ SET_CONTROL_LABEL(CONTROL_PORT_DIALOG_LABEL,
+ StringUtils::Format("$LOCALIZE[35111] - {}", m_gameClient->Name()));
+
+ m_portList->Initialize(m_gameClient);
+
+ UpdatePortList();
+
+ // Focus the port list
+ CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), CONTROL_PORT_LIST);
+ OnMessage(msgFocus);
+}
+
+void CGUIPortWindow::OnDeinitWindow(int nextWindowID)
+{
+ m_portList->Deinitialize();
+
+ m_gameClient.reset();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIPortWindow::FrameMove()
+{
+ CGUIDialog::FrameMove();
+
+ m_portList->FrameMove();
+}
+
+void CGUIPortWindow::UpdatePortList()
+{
+ m_portList->Refresh();
+}
+
+void CGUIPortWindow::FocusPortList()
+{
+ m_portList->SetFocused();
+}
+
+bool CGUIPortWindow::OnClickAction()
+{
+ return m_portList->OnSelect();
+}
+
+void CGUIPortWindow::ResetPorts()
+{
+ m_portList->ResetPorts();
+}
+
+void CGUIPortWindow::CloseDialog()
+{
+ Close();
+}
diff --git a/xbmc/games/ports/windows/GUIPortWindow.h b/xbmc/games/ports/windows/GUIPortWindow.h
new file mode 100644
index 0000000..3b98708
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortWindow.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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 "guilib/GUIDialog.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class IPortList;
+
+class CGUIPortWindow : public CGUIDialog
+{
+public:
+ CGUIPortWindow();
+ ~CGUIPortWindow() override;
+
+ // Implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // Implementation of CGUIWindow via CGUIDialog
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void FrameMove() override;
+
+private:
+ // Actions for port list
+ void UpdatePortList();
+ void FocusPortList();
+ bool OnClickAction();
+
+ // Actions for the available buttons
+ void ResetPorts();
+ void CloseDialog();
+
+ // GUI parameters
+ std::unique_ptr<IPortList> m_portList;
+
+ // Game parameters
+ GameClientPtr m_gameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/windows/IPortList.h b/xbmc/games/ports/windows/IPortList.h
new file mode 100644
index 0000000..e330c6e
--- /dev/null
+++ b/xbmc/games/ports/windows/IPortList.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 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"
+
+/*!
+ * \brief Controller port setup window
+ *
+ * The port setup window presents a list of ports and their attached
+ * controllers.
+ *
+ * The label2 of each port is the currently-connected controller. The user
+ * selects from all controllers that the port accepts (as given by the
+ * game-addon's topology.xml file).
+ *
+ * The controller topology is stored as a generic tree. Here we apply game logic
+ * to simplify controller selection.
+ */
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief A list populated by controller ports
+ */
+class IPortList
+{
+public:
+ virtual ~IPortList() = default;
+
+ /*!
+ * \brief Callback when the GUI window is loaded
+ */
+ virtual void OnWindowLoaded() = 0;
+
+ /*!
+ * \brief Callback when the GUI window is unloaded
+ */
+ virtual void OnWindowUnload() = 0;
+
+ /*!
+ * \brief Initialize resources
+ *
+ * \param gameClient The game client providing the ports
+ *
+ * \return True if the resource is initialized and can be used, false if the
+ * resource failed to initialize and must not be used
+ */
+ virtual bool Initialize(GameClientPtr gameClient) = 0;
+
+ /*!
+ * \brief Deinitialize resources
+ */
+ virtual void Deinitialize() = 0;
+
+ /*!
+ * \brief Query if a control with the given ID belongs to this list
+ */
+ virtual bool HasControl(int controlId) = 0;
+
+ /*!
+ * \brief Query the ID of the current control in this list
+ *
+ * \return The control ID, or -1 if no control is currently active
+ */
+ virtual int GetCurrentControl() = 0;
+
+ /*!
+ * \brief Refresh the contents of the list
+ */
+ virtual void Refresh() = 0;
+
+ /*!
+ * \brief Callback when a frame is rendered by the GUI
+ */
+ virtual void FrameMove() = 0;
+
+ /*!
+ * \brief The port list has been focused in the GUI
+ */
+ virtual void SetFocused() = 0;
+
+ /*!
+ * \brief The port list has been selected
+ *
+ * \brief True if a control was active, false of all controls were inactive
+ */
+ virtual bool OnSelect() = 0;
+
+ /*!
+ * \brief Reset the ports to their game add-on's default configuration
+ */
+ virtual void ResetPorts() = 0;
+};
+} // namespace GAME
+} // namespace KODI