summaryrefslogtreecommitdiffstats
path: root/xbmc/games
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--xbmc/games/CMakeLists.txt10
-rw-r--r--xbmc/games/GameServices.cpp63
-rw-r--r--xbmc/games/GameServices.h72
-rw-r--r--xbmc/games/GameSettings.cpp242
-rw-r--r--xbmc/games/GameSettings.h55
-rw-r--r--xbmc/games/GameTypes.h32
-rw-r--r--xbmc/games/GameUtils.cpp314
-rw-r--r--xbmc/games/GameUtils.h105
-rw-r--r--xbmc/games/addons/CMakeLists.txt14
-rw-r--r--xbmc/games/addons/GameClient.cpp730
-rw-r--r--xbmc/games/addons/GameClient.h264
-rw-r--r--xbmc/games/addons/GameClientCallbacks.h38
-rw-r--r--xbmc/games/addons/GameClientInGameSaves.cpp164
-rw-r--r--xbmc/games/addons/GameClientInGameSaves.h66
-rw-r--r--xbmc/games/addons/GameClientProperties.cpp292
-rw-r--r--xbmc/games/addons/GameClientProperties.h93
-rw-r--r--xbmc/games/addons/GameClientSubsystem.cpp70
-rw-r--r--xbmc/games/addons/GameClientSubsystem.h81
-rw-r--r--xbmc/games/addons/GameClientTranslator.cpp256
-rw-r--r--xbmc/games/addons/GameClientTranslator.h115
-rw-r--r--xbmc/games/addons/cheevos/CMakeLists.txt5
-rw-r--r--xbmc/games/addons/cheevos/GameClientCheevos.cpp191
-rw-r--r--xbmc/games/addons/cheevos/GameClientCheevos.h57
-rw-r--r--xbmc/games/addons/input/CMakeLists.txt23
-rw-r--r--xbmc/games/addons/input/GameClientController.cpp138
-rw-r--r--xbmc/games/addons/input/GameClientController.h71
-rw-r--r--xbmc/games/addons/input/GameClientDevice.cpp73
-rw-r--r--xbmc/games/addons/input/GameClientDevice.h77
-rw-r--r--xbmc/games/addons/input/GameClientHardware.cpp25
-rw-r--r--xbmc/games/addons/input/GameClientHardware.h43
-rw-r--r--xbmc/games/addons/input/GameClientInput.cpp697
-rw-r--r--xbmc/games/addons/input/GameClientInput.h160
-rw-r--r--xbmc/games/addons/input/GameClientJoystick.cpp205
-rw-r--r--xbmc/games/addons/input/GameClientJoystick.h101
-rw-r--r--xbmc/games/addons/input/GameClientKeyboard.cpp101
-rw-r--r--xbmc/games/addons/input/GameClientKeyboard.h76
-rw-r--r--xbmc/games/addons/input/GameClientMouse.cpp106
-rw-r--r--xbmc/games/addons/input/GameClientMouse.h74
-rw-r--r--xbmc/games/addons/input/GameClientPort.cpp72
-rw-r--r--xbmc/games/addons/input/GameClientPort.h103
-rw-r--r--xbmc/games/addons/input/GameClientTopology.cpp110
-rw-r--r--xbmc/games/addons/input/GameClientTopology.h50
-rw-r--r--xbmc/games/addons/streams/CMakeLists.txt14
-rw-r--r--xbmc/games/addons/streams/GameClientStreamAudio.cpp105
-rw-r--r--xbmc/games/addons/streams/GameClientStreamAudio.h52
-rw-r--r--xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp40
-rw-r--r--xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h29
-rw-r--r--xbmc/games/addons/streams/GameClientStreamVideo.cpp108
-rw-r--r--xbmc/games/addons/streams/GameClientStreamVideo.h49
-rw-r--r--xbmc/games/addons/streams/GameClientStreams.cpp118
-rw-r--r--xbmc/games/addons/streams/GameClientStreams.h55
-rw-r--r--xbmc/games/addons/streams/IGameClientStream.h77
-rw-r--r--xbmc/games/agents/CMakeLists.txt7
-rw-r--r--xbmc/games/agents/GameAgentManager.cpp547
-rw-r--r--xbmc/games/agents/GameAgentManager.h171
-rw-r--r--xbmc/games/controllers/CMakeLists.txt18
-rw-r--r--xbmc/games/controllers/Controller.cpp157
-rw-r--r--xbmc/games/controllers/Controller.h120
-rw-r--r--xbmc/games/controllers/ControllerDefinitions.h55
-rw-r--r--xbmc/games/controllers/ControllerIDs.h15
-rw-r--r--xbmc/games/controllers/ControllerLayout.cpp159
-rw-r--r--xbmc/games/controllers/ControllerLayout.h92
-rw-r--r--xbmc/games/controllers/ControllerManager.cpp122
-rw-r--r--xbmc/games/controllers/ControllerManager.h93
-rw-r--r--xbmc/games/controllers/ControllerTranslator.cpp744
-rw-r--r--xbmc/games/controllers/ControllerTranslator.h53
-rw-r--r--xbmc/games/controllers/ControllerTypes.h33
-rw-r--r--xbmc/games/controllers/DefaultController.cpp34
-rw-r--r--xbmc/games/controllers/DefaultController.h50
-rw-r--r--xbmc/games/controllers/dialogs/CMakeLists.txt15
-rw-r--r--xbmc/games/controllers/dialogs/ControllerInstaller.cpp138
-rw-r--r--xbmc/games/controllers/dialogs/ControllerInstaller.h28
-rw-r--r--xbmc/games/controllers/dialogs/ControllerSelect.cpp133
-rw-r--r--xbmc/games/controllers/dialogs/ControllerSelect.h44
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp84
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h54
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp130
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h65
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp132
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h47
-rw-r--r--xbmc/games/controllers/guicontrols/CMakeLists.txt28
-rw-r--r--xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp100
-rw-r--r--xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h49
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControlTypes.h29
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControllerButton.cpp26
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControllerButton.h29
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp86
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureButton.h68
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp36
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureControls.h38
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp51
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureFactory.h37
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp39
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h27
-rw-r--r--xbmc/games/controllers/guicontrols/GUIGameController.cpp64
-rw-r--r--xbmc/games/controllers/guicontrols/GUIGameController.h39
-rw-r--r--xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp59
-rw-r--r--xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h42
-rw-r--r--xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp87
-rw-r--r--xbmc/games/controllers/guicontrols/GUISelectKeyButton.h51
-rw-r--r--xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp88
-rw-r--r--xbmc/games/controllers/guicontrols/GUIThrottleButton.h44
-rw-r--r--xbmc/games/controllers/guicontrols/GUIWheelButton.cpp88
-rw-r--r--xbmc/games/controllers/guicontrols/GUIWheelButton.h44
-rw-r--r--xbmc/games/controllers/input/CMakeLists.txt11
-rw-r--r--xbmc/games/controllers/input/InputSink.cpp67
-rw-r--r--xbmc/games/controllers/input/InputSink.h53
-rw-r--r--xbmc/games/controllers/input/PhysicalFeature.cpp162
-rw-r--r--xbmc/games/controllers/input/PhysicalFeature.h65
-rw-r--r--xbmc/games/controllers/input/PhysicalTopology.cpp56
-rw-r--r--xbmc/games/controllers/input/PhysicalTopology.h61
-rw-r--r--xbmc/games/controllers/types/CMakeLists.txt12
-rw-r--r--xbmc/games/controllers/types/ControllerGrid.cpp242
-rw-r--r--xbmc/games/controllers/types/ControllerGrid.h167
-rw-r--r--xbmc/games/controllers/types/ControllerHub.cpp109
-rw-r--r--xbmc/games/controllers/types/ControllerHub.h53
-rw-r--r--xbmc/games/controllers/types/ControllerNode.cpp136
-rw-r--r--xbmc/games/controllers/types/ControllerNode.h118
-rw-r--r--xbmc/games/controllers/types/ControllerTree.h30
-rw-r--r--xbmc/games/controllers/windows/CMakeLists.txt15
-rw-r--r--xbmc/games/controllers/windows/GUIConfigurationWizard.cpp494
-rw-r--r--xbmc/games/controllers/windows/GUIConfigurationWizard.h117
-rw-r--r--xbmc/games/controllers/windows/GUIControllerDefines.h45
-rw-r--r--xbmc/games/controllers/windows/GUIControllerList.cpp239
-rw-r--r--xbmc/games/controllers/windows/GUIControllerList.h62
-rw-r--r--xbmc/games/controllers/windows/GUIControllerWindow.cpp376
-rw-r--r--xbmc/games/controllers/windows/GUIControllerWindow.h68
-rw-r--r--xbmc/games/controllers/windows/GUIFeatureList.cpp297
-rw-r--r--xbmc/games/controllers/windows/GUIFeatureList.h77
-rw-r--r--xbmc/games/controllers/windows/IConfigurationWindow.h262
-rw-r--r--xbmc/games/dialogs/CMakeLists.txt10
-rw-r--r--xbmc/games/dialogs/DialogGameDefines.h27
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectGameClient.cpp121
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectGameClient.h61
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectSavestate.cpp58
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectSavestate.h28
-rw-r--r--xbmc/games/dialogs/osd/CMakeLists.txt25
-rw-r--r--xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp54
-rw-r--r--xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h27
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSD.cpp81
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSD.h54
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp71
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSDHelp.h40
-rw-r--r--xbmc/games/dialogs/osd/DialogGameSaves.cpp500
-rw-r--r--xbmc/games/dialogs/osd/DialogGameSaves.h113
-rw-r--r--xbmc/games/dialogs/osd/DialogGameStretchMode.cpp128
-rw-r--r--xbmc/games/dialogs/osd/DialogGameStretchMode.h51
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp157
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoFilter.h47
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp109
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoRotation.h44
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp254
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoSelect.h84
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVolume.cpp149
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVolume.h80
-rw-r--r--xbmc/games/dialogs/osd/DialogInGameSaves.cpp426
-rw-r--r--xbmc/games/dialogs/osd/DialogInGameSaves.h79
-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
-rw-r--r--xbmc/games/tags/CMakeLists.txt5
-rw-r--r--xbmc/games/tags/GameInfoTag.cpp158
-rw-r--r--xbmc/games/tags/GameInfoTag.h112
-rw-r--r--xbmc/games/windows/CMakeLists.txt7
-rw-r--r--xbmc/games/windows/GUIViewStateWindowGames.cpp91
-rw-r--r--xbmc/games/windows/GUIViewStateWindowGames.h34
-rw-r--r--xbmc/games/windows/GUIWindowGames.cpp340
-rw-r--r--xbmc/games/windows/GUIWindowGames.h45
182 files changed, 19802 insertions, 0 deletions
diff --git a/xbmc/games/CMakeLists.txt b/xbmc/games/CMakeLists.txt
new file mode 100644
index 0000000..70b26f3
--- /dev/null
+++ b/xbmc/games/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES GameServices.cpp
+ GameSettings.cpp
+ GameUtils.cpp)
+
+set(HEADERS GameServices.h
+ GameSettings.h
+ GameTypes.h
+ GameUtils.h)
+
+core_add_library(games)
diff --git a/xbmc/games/GameServices.cpp b/xbmc/games/GameServices.cpp
new file mode 100644
index 0000000..099c9a9
--- /dev/null
+++ b/xbmc/games/GameServices.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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 "GameServices.h"
+
+#include "controllers/Controller.h"
+#include "controllers/ControllerManager.h"
+#include "games/GameSettings.h"
+#include "games/agents/GameAgentManager.h"
+#include "profiles/ProfileManager.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameServices::CGameServices(CControllerManager& controllerManager,
+ RETRO::CGUIGameRenderManager& renderManager,
+ PERIPHERALS::CPeripherals& peripheralManager,
+ const CProfileManager& profileManager,
+ CInputManager& inputManager)
+ : m_controllerManager(controllerManager),
+ m_gameRenderManager(renderManager),
+ m_profileManager(profileManager),
+ m_gameSettings(new CGameSettings()),
+ m_gameAgentManager(new CGameAgentManager(peripheralManager, inputManager))
+{
+}
+
+CGameServices::~CGameServices() = default;
+
+ControllerPtr CGameServices::GetController(const std::string& controllerId)
+{
+ return m_controllerManager.GetController(controllerId);
+}
+
+ControllerPtr CGameServices::GetDefaultController()
+{
+ return m_controllerManager.GetDefaultController();
+}
+
+ControllerPtr CGameServices::GetDefaultKeyboard()
+{
+ return m_controllerManager.GetDefaultKeyboard();
+}
+
+ControllerPtr CGameServices::GetDefaultMouse()
+{
+ return m_controllerManager.GetDefaultMouse();
+}
+
+ControllerVector CGameServices::GetControllers()
+{
+ return m_controllerManager.GetControllers();
+}
+
+std::string CGameServices::GetSavestatesFolder() const
+{
+ return m_profileManager.GetSavestatesFolder();
+}
diff --git a/xbmc/games/GameServices.h b/xbmc/games/GameServices.h
new file mode 100644
index 0000000..c736feb
--- /dev/null
+++ b/xbmc/games/GameServices.h
@@ -0,0 +1,72 @@
+/*
+ * 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 "controllers/ControllerTypes.h"
+
+#include <memory>
+#include <string>
+
+class CInputManager;
+class CProfileManager;
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+}
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIGameRenderManager;
+}
+
+namespace GAME
+{
+class CGameAgentManager;
+class CControllerManager;
+class CGameSettings;
+
+class CGameServices
+{
+public:
+ CGameServices(CControllerManager& controllerManager,
+ RETRO::CGUIGameRenderManager& renderManager,
+ PERIPHERALS::CPeripherals& peripheralManager,
+ const CProfileManager& profileManager,
+ CInputManager& inputManager);
+ ~CGameServices();
+
+ ControllerPtr GetController(const std::string& controllerId);
+ ControllerPtr GetDefaultController();
+ ControllerPtr GetDefaultKeyboard();
+ ControllerPtr GetDefaultMouse();
+ ControllerVector GetControllers();
+
+ std::string GetSavestatesFolder() const;
+
+ CGameSettings& GameSettings() { return *m_gameSettings; }
+
+ RETRO::CGUIGameRenderManager& GameRenderManager() { return m_gameRenderManager; }
+
+ CGameAgentManager& GameAgentManager() { return *m_gameAgentManager; }
+
+private:
+ // Construction parameters
+ CControllerManager& m_controllerManager;
+ RETRO::CGUIGameRenderManager& m_gameRenderManager;
+ const CProfileManager& m_profileManager;
+
+ // Game services
+ std::unique_ptr<CGameSettings> m_gameSettings;
+ std::unique_ptr<CGameAgentManager> m_gameAgentManager;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/GameSettings.cpp b/xbmc/games/GameSettings.cpp
new file mode 100644
index 0000000..acbccdc
--- /dev/null
+++ b/xbmc/games/GameSettings.cpp
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2012-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 "GameSettings.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/File.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <vector>
+
+using namespace KODI;
+using namespace GAME;
+
+namespace
+{
+const std::string SETTING_GAMES_ENABLE = "gamesgeneral.enable";
+const std::string SETTING_GAMES_SHOW_OSD_HELP = "gamesgeneral.showosdhelp";
+const std::string SETTING_GAMES_ENABLEAUTOSAVE = "gamesgeneral.enableautosave";
+const std::string SETTING_GAMES_ENABLEREWIND = "gamesgeneral.enablerewind";
+const std::string SETTING_GAMES_REWINDTIME = "gamesgeneral.rewindtime";
+const std::string SETTING_GAMES_ACHIEVEMENTS_USERNAME = "gamesachievements.username";
+const std::string SETTING_GAMES_ACHIEVEMENTS_PASSWORD = "gamesachievements.password";
+const std::string SETTING_GAMES_ACHIEVEMENTS_TOKEN = "gamesachievements.token";
+const std::string SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN = "gamesachievements.loggedin";
+
+constexpr auto LOGIN_TO_RETRO_ACHIEVEMENTS_URL_TEMPLATE =
+ "http://retroachievements.org/dorequest.php?r=login&u={}&p={}";
+constexpr auto GET_PATCH_DATA_URL_TEMPLATE =
+ "http://retroachievements.org/dorequest.php?r=patch&u={}&t={}&g=0";
+constexpr auto SUCCESS = "Success";
+constexpr auto TOKEN = "Token";
+} // namespace
+
+CGameSettings::CGameSettings()
+{
+ m_settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ m_settings->RegisterCallback(this, {SETTING_GAMES_ENABLEREWIND, SETTING_GAMES_REWINDTIME,
+ SETTING_GAMES_ACHIEVEMENTS_USERNAME,
+ SETTING_GAMES_ACHIEVEMENTS_PASSWORD,
+ SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN});
+}
+
+CGameSettings::~CGameSettings()
+{
+ m_settings->UnregisterCallback(this);
+}
+
+bool CGameSettings::GamesEnabled()
+{
+ return m_settings->GetBool(SETTING_GAMES_ENABLE);
+}
+
+bool CGameSettings::ShowOSDHelp()
+{
+ return m_settings->GetBool(SETTING_GAMES_SHOW_OSD_HELP);
+}
+
+void CGameSettings::SetShowOSDHelp(bool bShow)
+{
+ if (m_settings->GetBool(SETTING_GAMES_SHOW_OSD_HELP) != bShow)
+ {
+ m_settings->SetBool(SETTING_GAMES_SHOW_OSD_HELP, bShow);
+
+ //! @todo Asynchronous save
+ m_settings->Save();
+ }
+}
+
+void CGameSettings::ToggleGames()
+{
+ m_settings->ToggleBool(SETTING_GAMES_ENABLE);
+}
+
+bool CGameSettings::AutosaveEnabled()
+{
+ return m_settings->GetBool(SETTING_GAMES_ENABLEAUTOSAVE);
+}
+
+bool CGameSettings::RewindEnabled()
+{
+ return m_settings->GetBool(SETTING_GAMES_ENABLEREWIND);
+}
+
+unsigned int CGameSettings::MaxRewindTimeSec()
+{
+ int rewindTimeSec = m_settings->GetInt(SETTING_GAMES_REWINDTIME);
+
+ return static_cast<unsigned int>(std::max(rewindTimeSec, 0));
+}
+
+std::string CGameSettings::GetRAUsername() const
+{
+ return m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_USERNAME);
+}
+
+std::string CGameSettings::GetRAToken() const
+{
+ return m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN);
+}
+
+void CGameSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_GAMES_ENABLEREWIND || settingId == SETTING_GAMES_REWINDTIME)
+ {
+ SetChanged();
+ NotifyObservers(ObservableMessageSettingsChanged);
+ }
+ else if (settingId == SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN &&
+ std::dynamic_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ const std::string username = m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_USERNAME);
+ const std::string password = m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_PASSWORD);
+ std::string token = m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN);
+
+ token = LoginToRA(username, password, std::move(token));
+
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN, token);
+
+ if (!token.empty())
+ {
+ m_settings->SetBool(SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN, true);
+ }
+ else
+ {
+ if (settingId == SETTING_GAMES_ACHIEVEMENTS_PASSWORD)
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_PASSWORD, "");
+ m_settings->SetBool(SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN, false);
+ }
+
+ m_settings->Save();
+ }
+ else if (settingId == SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN &&
+ !std::dynamic_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN, "");
+ m_settings->Save();
+ }
+ else if (settingId == SETTING_GAMES_ACHIEVEMENTS_USERNAME ||
+ settingId == SETTING_GAMES_ACHIEVEMENTS_PASSWORD)
+ {
+ m_settings->SetBool(SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN, false);
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN, "");
+ m_settings->Save();
+ }
+}
+
+std::string CGameSettings::LoginToRA(const std::string& username,
+ const std::string& password,
+ std::string token) const
+{
+ if (username.empty() || password.empty())
+ return token;
+
+ XFILE::CFile request;
+ const CURL loginUrl(
+ StringUtils::Format(LOGIN_TO_RETRO_ACHIEVEMENTS_URL_TEMPLATE, username, password));
+
+ std::vector<uint8_t> response;
+ if (request.LoadFile(loginUrl, response) > 0)
+ {
+ std::string strResponse(response.begin(), response.end());
+ CVariant data(CVariant::VariantTypeObject);
+ if (CJSONVariantParser::Parse(strResponse, data))
+ {
+ if (data[SUCCESS].asBoolean())
+ {
+ token = data[TOKEN].asString();
+ if (!IsAccountVerified(username, token))
+ {
+ token.clear();
+ // "RetroAchievements", "Your account is not verified, please check your emails to complete your sign up"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35270, EventLevel::Error)));
+ }
+ }
+ else
+ {
+ token.clear();
+
+ // "RetroAchievements", "Incorrect User/Password!"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35265, EventLevel::Error)));
+ }
+ }
+ else
+ {
+ // "RetroAchievements", "Invalid response from server"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35267, EventLevel::Error)));
+
+ CLog::Log(LOGERROR, "Invalid server response: {}", strResponse);
+ }
+ }
+ else
+ {
+ // "RetroAchievements", "Failed to contact server"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35266, EventLevel::Error)));
+ }
+ return token;
+}
+
+bool CGameSettings::IsAccountVerified(const std::string& username, const std::string& token) const
+{
+ XFILE::CFile request;
+ const CURL getPatchFileUrl(StringUtils::Format(GET_PATCH_DATA_URL_TEMPLATE, username, token));
+ std::vector<uint8_t> response;
+ if (request.LoadFile(getPatchFileUrl, response) > 0)
+ {
+ std::string strResponse(response.begin(), response.end());
+ CVariant data(CVariant::VariantTypeObject);
+
+ if (CJSONVariantParser::Parse(strResponse, data))
+ {
+ return data[SUCCESS].asBoolean();
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/games/GameSettings.h b/xbmc/games/GameSettings.h
new file mode 100644
index 0000000..63ee67b
--- /dev/null
+++ b/xbmc/games/GameSettings.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012-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 "settings/lib/ISettingCallback.h"
+#include "utils/Observer.h"
+
+#include <string>
+
+class CSetting;
+class CSettings;
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameSettings : public ISettingCallback, public Observable
+{
+public:
+ CGameSettings();
+ ~CGameSettings() override;
+
+ // General settings
+ bool GamesEnabled();
+ bool ShowOSDHelp();
+ void SetShowOSDHelp(bool bShow);
+ void ToggleGames();
+ bool AutosaveEnabled();
+ bool RewindEnabled();
+ unsigned int MaxRewindTimeSec();
+ std::string GetRAUsername() const;
+ std::string GetRAToken() const;
+
+ // Inherited from ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+private:
+ std::string LoginToRA(const std::string& username,
+ const std::string& password,
+ std::string token) const;
+ bool IsAccountVerified(const std::string& username, const std::string& token) const;
+
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/GameTypes.h b/xbmc/games/GameTypes.h
new file mode 100644
index 0000000..3be3195
--- /dev/null
+++ b/xbmc/games/GameTypes.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015-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 <memory>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameClient;
+using GameClientPtr = std::shared_ptr<CGameClient>;
+using GameClientVector = std::vector<GameClientPtr>;
+
+class CGameClientPort;
+using GameClientPortPtr = std::unique_ptr<CGameClientPort>;
+using GameClientPortVec = std::vector<GameClientPortPtr>;
+
+class CGameClientDevice;
+using GameClientDevicePtr = std::unique_ptr<CGameClientDevice>;
+using GameClientDeviceVec = std::vector<GameClientDevicePtr>;
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/GameUtils.cpp b/xbmc/games/GameUtils.cpp
new file mode 100644
index 0000000..5eba00b
--- /dev/null
+++ b/xbmc/games/GameUtils.cpp
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2012-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 "GameUtils.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/Addon.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/BinaryAddonCache.h"
+#include "addons/addoninfo/AddonType.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "filesystem/SpecialProtocol.h"
+#include "games/addons/GameClient.h"
+#include "games/dialogs/GUIDialogSelectGameClient.h"
+#include "games/dialogs/GUIDialogSelectSavestate.h"
+#include "games/tags/GameInfoTag.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGameUtils::FillInGameClient(CFileItem& item, std::string& savestatePath)
+{
+ using namespace ADDON;
+
+ if (item.GetGameInfoTag()->GetGameClient().empty())
+ {
+ // If the fileitem is an add-on, fall back to that
+ if (item.HasAddonInfo() && item.GetAddonInfo()->Type() == AddonType::GAMEDLL)
+ {
+ item.GetGameInfoTag()->SetGameClient(item.GetAddonInfo()->ID());
+ }
+ else
+ {
+ if (!CGUIDialogSelectSavestate::ShowAndGetSavestate(item.GetPath(), savestatePath))
+ return false;
+
+ if (!savestatePath.empty())
+ {
+ RETRO::CSavestateDatabase db;
+ std::unique_ptr<RETRO::ISavestate> save = RETRO::CSavestateDatabase::AllocateSavestate();
+ db.GetSavestate(savestatePath, *save);
+ item.GetGameInfoTag()->SetGameClient(save->GameClientID());
+ }
+ else
+ {
+ // No game client specified, need to ask the user
+ GameClientVector candidates;
+ GameClientVector installable;
+ bool bHasVfsGameClient;
+ GetGameClients(item, candidates, installable, bHasVfsGameClient);
+
+ if (candidates.empty() && installable.empty())
+ {
+ // if: "This game can only be played directly from a hard drive or partition. Compressed files must be extracted."
+ // else: "This game isn't compatible with any available emulators."
+ int errorTextId = bHasVfsGameClient ? 35214 : 35212;
+
+ // "Failed to play game"
+ KODI::MESSAGING::HELPERS::ShowOKDialogText(CVariant{35210}, CVariant{errorTextId});
+ }
+ else if (candidates.size() == 1 && installable.empty())
+ {
+ // Only 1 option, avoid prompting the user
+ item.GetGameInfoTag()->SetGameClient(candidates[0]->ID());
+ }
+ else
+ {
+ std::string gameClient = CGUIDialogSelectGameClient::ShowAndGetGameClient(
+ item.GetPath(), candidates, installable);
+
+ if (!gameClient.empty())
+ item.GetGameInfoTag()->SetGameClient(gameClient);
+ }
+ }
+ }
+ }
+
+ const std::string gameClient = item.GetGameInfoTag()->GetGameClient();
+ if (gameClient.empty())
+ return false;
+
+ if (Install(gameClient))
+ {
+ // If the addon is disabled we need to enable it
+ if (!Enable(gameClient))
+ {
+ CLog::Log(LOGDEBUG, "Failed to enable game client {}", gameClient);
+ item.GetGameInfoTag()->SetGameClient("");
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Failed to install game client: {}", gameClient);
+ item.GetGameInfoTag()->SetGameClient("");
+ }
+
+ return !item.GetGameInfoTag()->GetGameClient().empty();
+}
+
+void CGameUtils::GetGameClients(const CFileItem& file,
+ GameClientVector& candidates,
+ GameClientVector& installable,
+ bool& bHasVfsGameClient)
+{
+ using namespace ADDON;
+
+ bHasVfsGameClient = false;
+
+ // Try to resolve path to a local file, as not all game clients support VFS
+ CURL translatedUrl(CSpecialProtocol::TranslatePath(file.GetPath()));
+
+ // Get local candidates
+ VECADDONS localAddons;
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ addonCache.GetAddons(localAddons, AddonType::GAMEDLL);
+
+ bool bVfs = false;
+ GetGameClients(localAddons, translatedUrl, candidates, bVfs);
+ bHasVfsGameClient |= bVfs;
+
+ // Get remote candidates
+ VECADDONS remoteAddons;
+ if (CServiceBroker::GetAddonMgr().GetInstallableAddons(remoteAddons, AddonType::GAMEDLL))
+ {
+ GetGameClients(remoteAddons, translatedUrl, installable, bVfs);
+ bHasVfsGameClient |= bVfs;
+ }
+
+ // Sort by name
+ //! @todo Move to presentation code
+ auto SortByName = [](const GameClientPtr& lhs, const GameClientPtr& rhs) {
+ std::string lhsName = lhs->Name();
+ std::string rhsName = rhs->Name();
+
+ StringUtils::ToLower(lhsName);
+ StringUtils::ToLower(rhsName);
+
+ return lhsName < rhsName;
+ };
+
+ std::sort(candidates.begin(), candidates.end(), SortByName);
+ std::sort(installable.begin(), installable.end(), SortByName);
+}
+
+void CGameUtils::GetGameClients(const ADDON::VECADDONS& addons,
+ const CURL& translatedUrl,
+ GameClientVector& candidates,
+ bool& bHasVfsGameClient)
+{
+ bHasVfsGameClient = false;
+
+ const std::string extension = URIUtils::GetExtension(translatedUrl.Get());
+
+ const bool bIsLocalFile =
+ (translatedUrl.GetProtocol() == "file" || translatedUrl.GetProtocol().empty());
+
+ for (auto& addon : addons)
+ {
+ GameClientPtr gameClient = std::static_pointer_cast<CGameClient>(addon);
+
+ // Filter by extension
+ if (!gameClient->IsExtensionValid(extension))
+ continue;
+
+ // Filter by VFS
+ if (!bIsLocalFile && !gameClient->SupportsVFS())
+ {
+ bHasVfsGameClient = true;
+ continue;
+ }
+
+ candidates.push_back(gameClient);
+ }
+}
+
+bool CGameUtils::HasGameExtension(const std::string& path)
+{
+ using namespace ADDON;
+
+ // Get filename from CURL so that top-level zip directories will become
+ // normal paths:
+ //
+ // zip://%2Fpath_to_zip_file.zip/ -> /path_to_zip_file.zip
+ //
+ std::string filename = CURL(path).GetFileNameWithoutPath();
+
+ // Get the file extension
+ std::string extension = URIUtils::GetExtension(filename);
+ if (extension.empty())
+ return false;
+
+ StringUtils::ToLower(extension);
+
+ // Look for a game client that supports this extension
+ VECADDONS gameClients;
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ addonCache.GetInstalledAddons(gameClients, AddonType::GAMEDLL);
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ if (gc->IsExtensionValid(extension))
+ return true;
+ }
+
+ // Check remote add-ons
+ gameClients.clear();
+ if (CServiceBroker::GetAddonMgr().GetInstallableAddons(gameClients, AddonType::GAMEDLL))
+ {
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ if (gc->IsExtensionValid(extension))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+std::set<std::string> CGameUtils::GetGameExtensions()
+{
+ using namespace ADDON;
+
+ std::set<std::string> extensions;
+
+ VECADDONS gameClients;
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ addonCache.GetAddons(gameClients, AddonType::GAMEDLL);
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ extensions.insert(gc->GetExtensions().begin(), gc->GetExtensions().end());
+ }
+
+ // Check remote add-ons
+ gameClients.clear();
+ if (CServiceBroker::GetAddonMgr().GetInstallableAddons(gameClients, AddonType::GAMEDLL))
+ {
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ extensions.insert(gc->GetExtensions().begin(), gc->GetExtensions().end());
+ }
+ }
+
+ return extensions;
+}
+
+bool CGameUtils::IsStandaloneGame(const ADDON::AddonPtr& addon)
+{
+ using namespace ADDON;
+
+ switch (addon->Type())
+ {
+ case AddonType::GAMEDLL:
+ {
+ return std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsStandalone();
+ }
+ case AddonType::SCRIPT:
+ {
+ return addon->HasType(AddonType::GAME);
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool CGameUtils::Install(const std::string& gameClient)
+{
+ // If the addon isn't installed we need to install it
+ bool installed = CServiceBroker::GetAddonMgr().IsAddonInstalled(gameClient);
+ if (!installed)
+ {
+ ADDON::AddonPtr installedAddon;
+ installed = ADDON::CAddonInstaller::GetInstance().InstallModal(
+ gameClient, installedAddon, ADDON::InstallModalPrompt::CHOICE_NO);
+ if (!installed)
+ {
+ CLog::Log(LOGERROR, "Game utils: Failed to install {}", gameClient);
+ // "Error"
+ // "Failed to install add-on."
+ MESSAGING::HELPERS::ShowOKDialogText(CVariant{257}, CVariant{35256});
+ }
+ }
+
+ return installed;
+}
+
+bool CGameUtils::Enable(const std::string& gameClient)
+{
+ bool bSuccess = true;
+
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(gameClient))
+ bSuccess = CServiceBroker::GetAddonMgr().EnableAddon(gameClient);
+
+ return bSuccess;
+}
diff --git a/xbmc/games/GameUtils.h b/xbmc/games/GameUtils.h
new file mode 100644
index 0000000..b49985c
--- /dev/null
+++ b/xbmc/games/GameUtils.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2012-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 "GameTypes.h"
+
+#include <set>
+#include <string>
+
+class CFileItem;
+class CURL;
+
+namespace ADDON
+{
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+} // namespace ADDON
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \ingroup games
+ * \brief Game related utilities.
+ */
+class CGameUtils
+{
+public:
+ /*!
+ * \brief Set the game client property, prompt the user for a savestate if there are any
+ * (savestates store the information of which game client created it).
+ * If there are no savestates or the user wants a new savestate, prompt the user
+ * for a game client.
+ *
+ * \param item The item with or without a game client in its info tag
+ * \param savestatePath Output. The path to the savestate selected. Empty if new savestate was
+ * selected
+ *
+ * \return True if the item has a valid game client ID in its info tag
+ */
+ static bool FillInGameClient(CFileItem& item, std::string& savestatePath);
+
+ /*!
+ * \brief Check if the file extension is supported by an add-on in
+ * a local or remote repository
+ *
+ * \param path The path of the game file
+ *
+ * \return true if the path's extension is supported by a known game client
+ */
+ static bool HasGameExtension(const std::string& path);
+
+ /*!
+ * \brief Get all game extensions
+ */
+ static std::set<std::string> GetGameExtensions();
+
+ /*!
+ * \brief Check if game script or game add-on can be launched directly
+ *
+ * \return true if the add-on can be launched, false otherwise
+ */
+ static bool IsStandaloneGame(const ADDON::AddonPtr& addon);
+
+private:
+ static void GetGameClients(const CFileItem& file,
+ GameClientVector& candidates,
+ GameClientVector& installable,
+ bool& bHasVfsGameClient);
+ static void GetGameClients(const ADDON::VECADDONS& addons,
+ const CURL& translatedUrl,
+ GameClientVector& candidates,
+ bool& bHasVfsGameClient);
+
+ /*!
+ * \brief Install the specified game client
+ *
+ * If the game client is not installed, a model dialog is shown installing
+ * the game client. If the installation fails, an error dialog is shown.
+ *
+ * \param gameClient The game client to install
+ *
+ * \return True if the game client is installed, false otherwise
+ */
+ static bool Install(const std::string& gameClient);
+
+ /*!
+ * \brief Enable the specified game client
+ *
+ * \param gameClient the game client to enable
+ *
+ * \return True if the game client is enabled, false otherwise
+ */
+ static bool Enable(const std::string& gameClient);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/CMakeLists.txt b/xbmc/games/addons/CMakeLists.txt
new file mode 100644
index 0000000..42ec18d
--- /dev/null
+++ b/xbmc/games/addons/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES GameClient.cpp
+ GameClientInGameSaves.cpp
+ GameClientProperties.cpp
+ GameClientSubsystem.cpp
+ GameClientTranslator.cpp)
+
+set(HEADERS GameClient.h
+ GameClientCallbacks.h
+ GameClientInGameSaves.h
+ GameClientProperties.h
+ GameClientSubsystem.h
+ GameClientTranslator.h)
+
+core_add_library(gameaddons)
diff --git a/xbmc/games/addons/GameClient.cpp b/xbmc/games/addons/GameClient.cpp
new file mode 100644
index 0000000..b3e7e01
--- /dev/null
+++ b/xbmc/games/addons/GameClient.cpp
@@ -0,0 +1,730 @@
+/*
+ * Copyright (C) 2012-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 "GameClient.h"
+
+#include "FileItem.h"
+#include "GameClientCallbacks.h"
+#include "GameClientInGameSaves.h"
+#include "GameClientProperties.h"
+#include "GameClientTranslator.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonManager.h"
+#include "addons/BinaryAddonCache.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "games/GameServices.h"
+#include "games/addons/cheevos/GameClientCheevos.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/addons/streams/GameClientStreams.h"
+#include "games/addons/streams/IGameClientStream.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstring>
+#include <iterator>
+#include <mutex>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+using namespace KODI::MESSAGING;
+
+#define EXTENSION_SEPARATOR "|"
+#define EXTENSION_WILDCARD "*"
+
+#define GAME_PROPERTY_EXTENSIONS "extensions"
+#define GAME_PROPERTY_SUPPORTS_VFS "supports_vfs"
+#define GAME_PROPERTY_SUPPORTS_STANDALONE "supports_standalone"
+
+// --- NormalizeExtension ------------------------------------------------------
+
+namespace
+{
+/*
+ * \brief Convert to lower case and canonicalize with a leading "."
+ */
+std::string NormalizeExtension(const std::string& strExtension)
+{
+ std::string ext = strExtension;
+
+ if (!ext.empty() && ext != EXTENSION_WILDCARD)
+ {
+ StringUtils::ToLower(ext);
+
+ if (ext[0] != '.')
+ ext.insert(0, ".");
+ }
+
+ return ext;
+}
+} // namespace
+
+// --- CGameClient -------------------------------------------------------------
+
+CGameClient::CGameClient(const ADDON::AddonInfoPtr& addonInfo)
+ : CAddonDll(addonInfo, ADDON::AddonType::GAMEDLL),
+ m_subsystems(CGameClientSubsystem::CreateSubsystems(*this, *m_ifc.game, m_critSection)),
+ m_bSupportsAllExtensions(false),
+ m_bIsPlaying(false),
+ m_serializeSize(0),
+ m_region(GAME_REGION_UNKNOWN)
+{
+ using namespace ADDON;
+
+ std::vector<std::string> extensions = StringUtils::Split(
+ Type(AddonType::GAMEDLL)->GetValue(GAME_PROPERTY_EXTENSIONS).asString(), EXTENSION_SEPARATOR);
+ std::transform(extensions.begin(), extensions.end(),
+ std::inserter(m_extensions, m_extensions.begin()), NormalizeExtension);
+
+ // Check for wildcard extension
+ if (m_extensions.find(EXTENSION_WILDCARD) != m_extensions.end())
+ {
+ m_bSupportsAllExtensions = true;
+ m_extensions.clear();
+ }
+
+ m_bSupportsVFS =
+ addonInfo->Type(AddonType::GAMEDLL)->GetValue(GAME_PROPERTY_SUPPORTS_VFS).asBoolean();
+ m_bSupportsStandalone =
+ addonInfo->Type(AddonType::GAMEDLL)->GetValue(GAME_PROPERTY_SUPPORTS_STANDALONE).asBoolean();
+
+ std::tie(m_emulatorName, m_platforms) = ParseLibretroName(Name());
+}
+
+CGameClient::~CGameClient(void)
+{
+ CloseFile();
+ CGameClientSubsystem::DestroySubsystems(m_subsystems);
+}
+
+std::string CGameClient::LibPath() const
+{
+ // If the game client requires a proxy, load its DLL instead
+ if (m_ifc.game->props->proxy_dll_count > 0)
+ return GetDllPath(m_ifc.game->props->proxy_dll_paths[0]);
+
+ return CAddonDll::LibPath();
+}
+
+ADDON::AddonPtr CGameClient::GetRunningInstance() const
+{
+ using namespace ADDON;
+
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ return addonCache.GetAddonInstance(ID(), Type());
+}
+
+bool CGameClient::SupportsPath() const
+{
+ return !m_extensions.empty() || m_bSupportsAllExtensions;
+}
+
+bool CGameClient::IsExtensionValid(const std::string& strExtension) const
+{
+ if (strExtension.empty())
+ return false;
+
+ if (SupportsAllExtensions())
+ return true;
+
+ return m_extensions.find(NormalizeExtension(strExtension)) != m_extensions.end();
+}
+
+bool CGameClient::Initialize(void)
+{
+ using namespace XFILE;
+
+ // Ensure user profile directory exists for add-on
+ if (!CDirectory::Exists(Profile()))
+ CDirectory::Create(Profile());
+
+ // Ensure directory exists for savestates
+ const CGameServices& gameServices = CServiceBroker::GetGameServices();
+ std::string savestatesDir = URIUtils::AddFileToFolder(gameServices.GetSavestatesFolder(), ID());
+ if (!CDirectory::Exists(savestatesDir))
+ CDirectory::Create(savestatesDir);
+
+ if (!AddonProperties().InitializeProperties())
+ return false;
+
+ m_ifc.game->toKodi->kodiInstance = this;
+ m_ifc.game->toKodi->CloseGame = cb_close_game;
+ m_ifc.game->toKodi->OpenStream = cb_open_stream;
+ m_ifc.game->toKodi->GetStreamBuffer = cb_get_stream_buffer;
+ m_ifc.game->toKodi->AddStreamData = cb_add_stream_data;
+ m_ifc.game->toKodi->ReleaseStreamBuffer = cb_release_stream_buffer;
+ m_ifc.game->toKodi->CloseStream = cb_close_stream;
+ m_ifc.game->toKodi->HwGetProcAddress = cb_hw_get_proc_address;
+ m_ifc.game->toKodi->InputEvent = cb_input_event;
+
+ memset(m_ifc.game->toAddon, 0, sizeof(KodiToAddonFuncTable_Game));
+
+ if (CreateInstance(&m_ifc) == ADDON_STATUS_OK)
+ {
+ Input().Initialize();
+ LogAddonProperties();
+ return true;
+ }
+
+ return false;
+}
+
+void CGameClient::Unload()
+{
+ Input().Deinitialize();
+
+ DestroyInstance(&m_ifc);
+}
+
+bool CGameClient::OpenFile(const CFileItem& file,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input)
+{
+ // Check if we should open in standalone mode
+ if (file.GetPath().empty())
+ return false;
+
+ // Some cores "succeed" to load the file even if it doesn't exist
+ if (!CFileUtils::Exists(file.GetPath()))
+ {
+
+ // Failed to play game
+ // The required files can't be found.
+ HELPERS::ShowOKDialogText(CVariant{35210}, CVariant{g_localizeStrings.Get(35219)});
+ return false;
+ }
+
+ // Resolve special:// URLs
+ CURL translatedUrl(CSpecialProtocol::TranslatePath(file.GetPath()));
+
+ // Remove file:// from URLs if add-on doesn't support VFS
+ if (!m_bSupportsVFS)
+ {
+ if (translatedUrl.GetProtocol() == "file")
+ translatedUrl.SetProtocol("");
+ }
+
+ std::string path = translatedUrl.Get();
+ CLog::Log(LOGDEBUG, "GameClient: Loading {}", CURL::GetRedacted(path));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!Initialized())
+ return false;
+
+ CloseFile();
+
+ GAME_ERROR error = GAME_ERROR_FAILED;
+
+ // Loading the game might require the stream subsystem to be initialized
+ Streams().Initialize(streamManager);
+
+ try
+ {
+ LogError(error = m_ifc.game->toAddon->LoadGame(m_ifc.game, path.c_str()), "LoadGame()");
+ }
+ catch (...)
+ {
+ LogException("LoadGame()");
+ }
+
+ if (error != GAME_ERROR_NO_ERROR)
+ {
+ NotifyError(error);
+ Streams().Deinitialize();
+ return false;
+ }
+
+ if (!InitializeGameplay(file.GetPath(), streamManager, input))
+ {
+ Streams().Deinitialize();
+ return false;
+ }
+
+ return true;
+}
+
+bool CGameClient::OpenStandalone(RETRO::IStreamManager& streamManager, IGameInputCallback* input)
+{
+ CLog::Log(LOGDEBUG, "GameClient: Loading {} in standalone mode", ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!Initialized())
+ return false;
+
+ CloseFile();
+
+ GAME_ERROR error = GAME_ERROR_FAILED;
+
+ // Loading the game might require the stream subsystem to be initialized
+ Streams().Initialize(streamManager);
+
+ try
+ {
+ LogError(error = m_ifc.game->toAddon->LoadStandalone(m_ifc.game), "LoadStandalone()");
+ }
+ catch (...)
+ {
+ LogException("LoadStandalone()");
+ }
+
+ if (error != GAME_ERROR_NO_ERROR)
+ {
+ NotifyError(error);
+ Streams().Deinitialize();
+ return false;
+ }
+
+ if (!InitializeGameplay("", streamManager, input))
+ {
+ Streams().Deinitialize();
+ return false;
+ }
+
+ return true;
+}
+
+bool CGameClient::InitializeGameplay(const std::string& gamePath,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input)
+{
+ if (LoadGameInfo())
+ {
+ Input().Start(input);
+
+ m_bIsPlaying = true;
+ m_gamePath = gamePath;
+ m_input = input;
+
+ m_inGameSaves.reset(new CGameClientInGameSaves(this, m_ifc.game));
+ m_inGameSaves->Load();
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClient::LoadGameInfo()
+{
+ bool bRequiresGameLoop;
+ try
+ {
+ bRequiresGameLoop = m_ifc.game->toAddon->RequiresGameLoop(m_ifc.game);
+ }
+ catch (...)
+ {
+ LogException("RequiresGameLoop()");
+ return false;
+ }
+
+ // Get information about system timings
+ // Can be called only after retro_load_game()
+ game_system_timing timingInfo = {};
+
+ bool bSuccess = false;
+ try
+ {
+ bSuccess =
+ LogError(m_ifc.game->toAddon->GetGameTiming(m_ifc.game, &timingInfo), "GetGameTiming()");
+ }
+ catch (...)
+ {
+ LogException("GetGameTiming()");
+ }
+
+ if (!bSuccess)
+ {
+ CLog::Log(LOGERROR, "GameClient: Failed to get timing info");
+ return false;
+ }
+
+ GAME_REGION region;
+ try
+ {
+ region = m_ifc.game->toAddon->GetRegion(m_ifc.game);
+ }
+ catch (...)
+ {
+ LogException("GetRegion()");
+ return false;
+ }
+
+ size_t serializeSize;
+ try
+ {
+ serializeSize = m_ifc.game->toAddon->SerializeSize(m_ifc.game);
+ }
+ catch (...)
+ {
+ LogException("SerializeSize()");
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "GAME: ---------------------------------------");
+ CLog::Log(LOGINFO, "GAME: Game loop: {}", bRequiresGameLoop ? "true" : "false");
+ CLog::Log(LOGINFO, "GAME: FPS: {:f}", timingInfo.fps);
+ CLog::Log(LOGINFO, "GAME: Sample Rate: {:f}", timingInfo.sample_rate);
+ CLog::Log(LOGINFO, "GAME: Region: {}", CGameClientTranslator::TranslateRegion(region));
+ CLog::Log(LOGINFO, "GAME: Savestate size: {}", serializeSize);
+ CLog::Log(LOGINFO, "GAME: ---------------------------------------");
+
+ m_bRequiresGameLoop = bRequiresGameLoop;
+ m_serializeSize = serializeSize;
+ m_framerate = timingInfo.fps;
+ m_samplerate = timingInfo.sample_rate;
+ m_region = region;
+
+ return true;
+}
+
+void CGameClient::NotifyError(GAME_ERROR error)
+{
+ std::string missingResource;
+
+ if (error == GAME_ERROR_RESTRICTED)
+ missingResource = GetMissingResource();
+
+ if (!missingResource.empty())
+ {
+ // Failed to play game
+ // This game requires the following add-on: %s
+ HELPERS::ShowOKDialogText(CVariant{35210}, CVariant{StringUtils::Format(
+ g_localizeStrings.Get(35211), missingResource)});
+ }
+ else
+ {
+ // Failed to play game
+ // The emulator "%s" had an internal error.
+ HELPERS::ShowOKDialogText(CVariant{35210},
+ CVariant{StringUtils::Format(g_localizeStrings.Get(35213), Name())});
+ }
+}
+
+std::string CGameClient::GetMissingResource()
+{
+ using namespace ADDON;
+
+ std::string strAddonId;
+
+ const auto& dependencies = GetDependencies();
+ for (auto it = dependencies.begin(); it != dependencies.end(); ++it)
+ {
+ const std::string& strDependencyId = it->id;
+ if (StringUtils::StartsWith(strDependencyId, "resource.games"))
+ {
+ AddonPtr addon;
+ const bool bInstalled =
+ CServiceBroker::GetAddonMgr().GetAddon(strDependencyId, addon, OnlyEnabled::CHOICE_YES);
+ if (!bInstalled)
+ {
+ strAddonId = strDependencyId;
+ break;
+ }
+ }
+ }
+
+ return strAddonId;
+}
+
+void CGameClient::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ LogError(m_ifc.game->toAddon->Reset(m_ifc.game), "Reset()");
+ }
+ catch (...)
+ {
+ LogException("Reset()");
+ }
+ }
+}
+
+void CGameClient::CloseFile()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsPlaying)
+ {
+ m_inGameSaves->Save();
+ m_inGameSaves.reset();
+
+ m_bIsPlaying = false;
+ m_gamePath.clear();
+ m_serializeSize = 0;
+ m_input = nullptr;
+
+ Input().Stop();
+
+ try
+ {
+ LogError(m_ifc.game->toAddon->UnloadGame(m_ifc.game), "UnloadGame()");
+ }
+ catch (...)
+ {
+ LogException("UnloadGame()");
+ }
+
+ Streams().Deinitialize();
+ }
+}
+
+void CGameClient::RunFrame()
+{
+ IGameInputCallback* input;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ input = m_input;
+ }
+
+ if (input)
+ input->PollInput();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ LogError(m_ifc.game->toAddon->RunFrame(m_ifc.game), "RunFrame()");
+ }
+ catch (...)
+ {
+ LogException("RunFrame()");
+ }
+ }
+}
+
+bool CGameClient::Serialize(uint8_t* data, size_t size)
+{
+ if (data == nullptr || size == 0)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bSuccess = false;
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ bSuccess = LogError(m_ifc.game->toAddon->Serialize(m_ifc.game, data, size), "Serialize()");
+ }
+ catch (...)
+ {
+ LogException("Serialize()");
+ }
+ }
+
+ return bSuccess;
+}
+
+bool CGameClient::Deserialize(const uint8_t* data, size_t size)
+{
+ if (data == nullptr || size == 0)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bSuccess = false;
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ bSuccess =
+ LogError(m_ifc.game->toAddon->Deserialize(m_ifc.game, data, size), "Deserialize()");
+ }
+ catch (...)
+ {
+ LogException("Deserialize()");
+ }
+ }
+
+ return bSuccess;
+}
+
+void CGameClient::LogAddonProperties(void) const
+{
+ CLog::Log(LOGINFO, "GAME: ------------------------------------");
+ CLog::Log(LOGINFO, "GAME: Loaded DLL for {}", ID());
+ CLog::Log(LOGINFO, "GAME: Client: {}", Name());
+ CLog::Log(LOGINFO, "GAME: Version: {}", Version().asString());
+ CLog::Log(LOGINFO, "GAME: Valid extensions: {}", StringUtils::Join(m_extensions, " "));
+ CLog::Log(LOGINFO, "GAME: Supports VFS: {}", m_bSupportsVFS);
+ CLog::Log(LOGINFO, "GAME: Supports standalone: {}", m_bSupportsStandalone);
+ CLog::Log(LOGINFO, "GAME: ------------------------------------");
+}
+
+bool CGameClient::LogError(GAME_ERROR error, const char* strMethod) const
+{
+ if (error != GAME_ERROR_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "GAME - {} - addon '{}' returned an error: {}", strMethod, ID(),
+ CGameClientTranslator::ToString(error));
+ return false;
+ }
+ return true;
+}
+
+void CGameClient::LogException(const char* strFunctionName) const
+{
+ CLog::Log(LOGERROR, "GAME: exception caught while trying to call '{}' on add-on {}",
+ strFunctionName, ID());
+ CLog::Log(LOGERROR, "Please contact the developer of this add-on: {}", Author());
+}
+
+void CGameClient::cb_close_game(KODI_HANDLE kodiInstance)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_STOP)));
+}
+
+KODI_GAME_STREAM_HANDLE CGameClient::cb_open_stream(KODI_HANDLE kodiInstance,
+ const game_stream_properties* properties)
+{
+ if (properties == nullptr)
+ return nullptr;
+
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (gameClient == nullptr)
+ return nullptr;
+
+ return gameClient->Streams().OpenStream(*properties);
+}
+
+bool CGameClient::cb_get_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ unsigned int width,
+ unsigned int height,
+ game_stream_buffer* buffer)
+{
+ if (buffer == nullptr)
+ return false;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return false;
+
+ return gameClientStream->GetBuffer(width, height, *buffer);
+}
+
+void CGameClient::cb_add_stream_data(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ const game_stream_packet* packet)
+{
+ if (packet == nullptr)
+ return;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return;
+
+ gameClientStream->AddData(*packet);
+}
+
+void CGameClient::cb_release_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ game_stream_buffer* buffer)
+{
+ if (buffer == nullptr)
+ return;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return;
+
+ gameClientStream->ReleaseBuffer(*buffer);
+}
+
+void CGameClient::cb_close_stream(KODI_HANDLE kodiInstance, KODI_GAME_STREAM_HANDLE stream)
+{
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (gameClient == nullptr)
+ return;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return;
+
+ gameClient->Streams().CloseStream(gameClientStream);
+}
+
+game_proc_address_t CGameClient::cb_hw_get_proc_address(KODI_HANDLE kodiInstance, const char* sym)
+{
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (!gameClient)
+ return nullptr;
+
+ //! @todo
+ return nullptr;
+}
+
+bool CGameClient::cb_input_event(KODI_HANDLE kodiInstance, const game_input_event* event)
+{
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (!gameClient)
+ return false;
+
+ if (event == nullptr)
+ return false;
+
+ return gameClient->Input().ReceiveInputEvent(*event);
+}
+
+std::pair<std::string, std::string> CGameClient::ParseLibretroName(const std::string& addonName)
+{
+ std::string emulatorName;
+ std::string platforms;
+
+ // libretro has a de-facto standard for naming their cores. If the
+ // core emulates one or more platforms, then the format is:
+ //
+ // "Platforms (Emulator name)"
+ //
+ // Otherwise, the format is just the name with no platforms:
+ //
+ // "Emulator name"
+ //
+ // The has been observed for all 130 cores we package.
+ //
+ size_t beginPos = addonName.find('(');
+ size_t endPos = addonName.find(')');
+
+ if (beginPos != std::string::npos && endPos != std::string::npos && beginPos < endPos)
+ {
+ emulatorName = addonName.substr(beginPos + 1, endPos - beginPos - 1);
+ platforms = addonName.substr(0, beginPos);
+ StringUtils::TrimRight(platforms);
+ }
+ else
+ {
+ emulatorName = addonName;
+ platforms.clear();
+ }
+
+ return std::make_pair(emulatorName, platforms);
+}
diff --git a/xbmc/games/addons/GameClient.h b/xbmc/games/addons/GameClient.h
new file mode 100644
index 0000000..12a0b61
--- /dev/null
+++ b/xbmc/games/addons/GameClient.h
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2012-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 "GameClientSubsystem.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "threads/CriticalSection.h"
+
+#include <atomic>
+#include <memory>
+#include <set>
+#include <stdint.h>
+#include <string>
+#include <utility>
+
+class CFileItem;
+
+namespace KODI
+{
+namespace RETRO
+{
+class IStreamManager;
+}
+
+namespace GAME
+{
+
+class CGameClientCheevos;
+class CGameClientInGameSaves;
+class CGameClientInput;
+class CGameClientProperties;
+class IGameInputCallback;
+
+/*!
+ * \ingroup games
+ * \brief Helper class to have "C" struct created before other parts becomes his pointer.
+ */
+class CGameClientStruct
+{
+public:
+ CGameClientStruct()
+ {
+ // Create "C" interface structures, used as own parts to prevent API problems on update
+ KODI_ADDON_INSTANCE_INFO* info = new KODI_ADDON_INSTANCE_INFO();
+ info->id = "";
+ info->version = kodi::addon::GetTypeVersion(ADDON_INSTANCE_GAME);
+ info->type = ADDON_INSTANCE_GAME;
+ info->kodi = this;
+ info->parent = nullptr;
+ info->first_instance = true;
+ info->functions = new KODI_ADDON_INSTANCE_FUNC_CB();
+ m_ifc.info = info;
+ m_ifc.functions = new KODI_ADDON_INSTANCE_FUNC();
+
+ m_ifc.game = new AddonInstance_Game;
+ m_ifc.game->props = new AddonProps_Game();
+ m_ifc.game->toKodi = new AddonToKodiFuncTable_Game();
+ m_ifc.game->toAddon = new KodiToAddonFuncTable_Game();
+ }
+
+ ~CGameClientStruct()
+ {
+ delete m_ifc.functions;
+ if (m_ifc.info)
+ delete m_ifc.info->functions;
+ delete m_ifc.info;
+ if (m_ifc.game)
+ {
+ delete m_ifc.game->toAddon;
+ delete m_ifc.game->toKodi;
+ delete m_ifc.game->props;
+ delete m_ifc.game;
+ }
+ }
+
+ KODI_ADDON_INSTANCE_STRUCT m_ifc;
+};
+
+/*!
+ * \ingroup games
+ * \brief Interface between Kodi and Game add-ons.
+ *
+ * The game add-on system is extremely large. To make the code more manageable,
+ * a subsystem pattern has been put in place. This pattern takes functionality
+ * that would normally be placed in this class, and puts it in another class
+ * (a "subsystem").
+ *
+ * The architecture is relatively simple. Subsystems are placed in a flat
+ * struct and accessed by calling the subsystem name. For example,
+ * historically, OpenJoystick() was a member of this class. Now, the function
+ * is called like Input().OpenJoystick().
+ *
+ * Although this pattern adds a layer of complexity, it enforces modularity and
+ * separation of concerns by making it very clear when one subsystem becomes
+ * dependent on another. Subsystems are all given access to each other by the
+ * calling mechanism. However, calling a subsystem creates a dependency on it,
+ * and an engineering decision must be made to justify the dependency.
+ *
+ * CONTRIBUTING
+ *
+ * If you wish to contribute, a beneficial task would be to refactor anything
+ * in this class into a new subsystem:
+ *
+ * Using line count as a heuristic, the subsystem pattern has shrunk the .cpp
+ * from 1,200 lines to just over 600. Reducing this further is the challenge.
+ * You must now choose whether to accept.
+ */
+class CGameClient : public ADDON::CAddonDll, private CGameClientStruct
+{
+public:
+ explicit CGameClient(const ADDON::AddonInfoPtr& addonInfo);
+
+ ~CGameClient() override;
+
+ // Game subsystems (const)
+ const CGameClientCheevos& Cheevos() const { return *m_subsystems.Cheevos; }
+ const CGameClientInput& Input() const { return *m_subsystems.Input; }
+ const CGameClientProperties& AddonProperties() const { return *m_subsystems.AddonProperties; }
+ const CGameClientStreams& Streams() const { return *m_subsystems.Streams; }
+
+ // Game subsystems (mutable)
+ CGameClientCheevos& Cheevos() { return *m_subsystems.Cheevos; }
+ CGameClientInput& Input() { return *m_subsystems.Input; }
+ CGameClientProperties& AddonProperties() { return *m_subsystems.AddonProperties; }
+ CGameClientStreams& Streams() { return *m_subsystems.Streams; }
+
+ // Implementation of IAddon via CAddonDll
+ std::string LibPath() const override;
+ ADDON::AddonPtr GetRunningInstance() const override;
+
+ // Query properties of the game client
+ bool SupportsStandalone() const { return m_bSupportsStandalone; }
+ bool SupportsPath() const;
+ bool SupportsVFS() const { return m_bSupportsVFS; }
+ const std::set<std::string>& GetExtensions() const { return m_extensions; }
+ bool SupportsAllExtensions() const { return m_bSupportsAllExtensions; }
+ bool IsExtensionValid(const std::string& strExtension) const;
+ const std::string& GetEmulatorName() const { return m_emulatorName; }
+ const std::string& GetPlatforms() const { return m_platforms; }
+
+ // Start/stop gameplay
+ bool Initialize(void);
+ void Unload();
+ bool OpenFile(const CFileItem& file,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input);
+ bool OpenStandalone(RETRO::IStreamManager& streamManager, IGameInputCallback* input);
+ void Reset();
+ void CloseFile();
+ const std::string& GetGamePath() const { return m_gamePath; }
+
+ // Playback control
+ bool RequiresGameLoop() const { return m_bRequiresGameLoop; }
+ bool IsPlaying() const { return m_bIsPlaying; }
+ size_t GetSerializeSize() const { return m_serializeSize; }
+ double GetFrameRate() const { return m_framerate; }
+ double GetSampleRate() const { return m_samplerate; }
+ void RunFrame();
+
+ // Access memory
+ size_t SerializeSize() const { return m_serializeSize; }
+ bool Serialize(uint8_t* data, size_t size);
+ bool Deserialize(const uint8_t* data, size_t size);
+
+ /*!
+ * @brief To get the interface table used between addon and kodi
+ * @todo This function becomes removed after old callback library system
+ * is removed.
+ */
+ AddonInstance_Game* GetInstanceInterface() { return m_ifc.game; }
+
+ // Helper functions
+ bool LogError(GAME_ERROR error, const char* strMethod) const;
+ void LogException(const char* strFunctionName) const;
+
+private:
+ // Private gameplay functions
+ bool InitializeGameplay(const std::string& gamePath,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input);
+ bool LoadGameInfo();
+ void NotifyError(GAME_ERROR error);
+ std::string GetMissingResource();
+
+ // Helper functions
+ void LogAddonProperties(void) const;
+
+ /*!
+ * @brief Callback functions from addon to kodi
+ */
+ //@{
+ static void cb_close_game(KODI_HANDLE kodiInstance);
+ static KODI_GAME_STREAM_HANDLE cb_open_stream(KODI_HANDLE kodiInstance,
+ const game_stream_properties* properties);
+ static bool cb_get_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ unsigned int width,
+ unsigned int height,
+ game_stream_buffer* buffer);
+ static void cb_add_stream_data(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ const game_stream_packet* packet);
+ static void cb_release_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ game_stream_buffer* buffer);
+ static void cb_close_stream(KODI_HANDLE kodiInstance, KODI_GAME_STREAM_HANDLE stream);
+ static game_proc_address_t cb_hw_get_proc_address(KODI_HANDLE kodiInstance, const char* sym);
+ static bool cb_input_event(KODI_HANDLE kodiInstance, const game_input_event* event);
+ //@}
+
+ /*!
+ * \brief Parse the name of a libretro game add-on into its emulator name
+ * and platforms
+ *
+ * \param addonName The name of the add-on, e.g. "Nintendo - SNES / SFC (Snes9x 2002)"
+ *
+ * \return A tuple of two strings:
+ * - first: the emulator name, e.g. "Snes9x 2002"
+ * - second: the platforms, e.g. "Nintendo - SNES / SFC"
+ *
+ * For cores that don't emulate a platform, such as 2048 with the add-on name
+ * "2048", then the emulator name will be the add-on name and platforms
+ * will be empty, e.g.:
+ * - first: "2048"
+ * - second: ""
+ */
+ static std::pair<std::string, std::string> ParseLibretroName(const std::string& addonName);
+
+ // Game subsystems
+ GameClientSubsystems m_subsystems;
+
+ // Game API xml parameters
+ bool m_bSupportsVFS;
+ bool m_bSupportsStandalone;
+ std::set<std::string> m_extensions;
+ bool m_bSupportsAllExtensions;
+ std::string m_emulatorName;
+ std::string m_platforms;
+
+ // Properties of the current playing file
+ std::atomic_bool m_bIsPlaying; // True between OpenFile() and CloseFile()
+ std::string m_gamePath;
+ bool m_bRequiresGameLoop = false;
+ size_t m_serializeSize;
+ IGameInputCallback* m_input = nullptr; // The input callback passed to OpenFile()
+ double m_framerate = 0.0; // Video frame rate (fps)
+ double m_samplerate = 0.0; // Audio sample rate (Hz)
+ GAME_REGION m_region; // Region of the loaded game
+
+ // In-game saves
+ std::unique_ptr<CGameClientInGameSaves> m_inGameSaves;
+
+ CCriticalSection m_critSection;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientCallbacks.h b/xbmc/games/addons/GameClientCallbacks.h
new file mode 100644
index 0000000..f4bbe2d
--- /dev/null
+++ b/xbmc/games/addons/GameClientCallbacks.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-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
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Input callbacks
+ *
+ * @todo Remove this file when Game API is updated for input polling
+ */
+class IGameInputCallback
+{
+public:
+ virtual ~IGameInputCallback() = default;
+
+ /*!
+ * \brief Return true if the input source accepts input
+ *
+ * \return True if input should be processed, false otherwise
+ */
+ virtual bool AcceptsInput() const = 0;
+
+ /*!
+ * \brief Poll the input source for input
+ */
+ virtual void PollInput() = 0;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientInGameSaves.cpp b/xbmc/games/addons/GameClientInGameSaves.cpp
new file mode 100644
index 0000000..802ebb7
--- /dev/null
+++ b/xbmc/games/addons/GameClientInGameSaves.cpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2016-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 "GameClientInGameSaves.h"
+
+#include "GameClient.h"
+#include "GameClientTranslator.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "games/GameServices.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <assert.h>
+
+using namespace KODI;
+using namespace GAME;
+
+#define INGAME_SAVES_DIRECTORY "InGameSaves"
+#define INGAME_SAVES_EXTENSION_SAVE_RAM ".sav"
+#define INGAME_SAVES_EXTENSION_RTC ".rtc"
+
+CGameClientInGameSaves::CGameClientInGameSaves(CGameClient* addon,
+ const AddonInstance_Game* dllStruct)
+ : m_gameClient(addon), m_dllStruct(dllStruct)
+{
+ assert(m_gameClient != nullptr);
+ assert(m_dllStruct != nullptr);
+}
+
+void CGameClientInGameSaves::Load()
+{
+ Load(GAME_MEMORY_SAVE_RAM);
+ Load(GAME_MEMORY_RTC);
+}
+
+void CGameClientInGameSaves::Save()
+{
+ Save(GAME_MEMORY_SAVE_RAM);
+ Save(GAME_MEMORY_RTC);
+}
+
+std::string CGameClientInGameSaves::GetPath(GAME_MEMORY memoryType)
+{
+ const CGameServices& gameServices = CServiceBroker::GetGameServices();
+ std::string path =
+ URIUtils::AddFileToFolder(gameServices.GetSavestatesFolder(), INGAME_SAVES_DIRECTORY);
+ if (!XFILE::CDirectory::Exists(path))
+ XFILE::CDirectory::Create(path);
+
+ // Append save game filename
+ std::string gamePath = URIUtils::GetFileName(m_gameClient->GetGamePath());
+ path = URIUtils::AddFileToFolder(path, gamePath.empty() ? m_gameClient->ID() : gamePath);
+
+ // Append file extension
+ switch (memoryType)
+ {
+ case GAME_MEMORY_SAVE_RAM:
+ return path + INGAME_SAVES_EXTENSION_SAVE_RAM;
+ case GAME_MEMORY_RTC:
+ return path + INGAME_SAVES_EXTENSION_RTC;
+ default:
+ break;
+ }
+ return std::string();
+}
+
+void CGameClientInGameSaves::Load(GAME_MEMORY memoryType)
+{
+ uint8_t* gameMemory = nullptr;
+ size_t size = 0;
+
+ try
+ {
+ m_dllStruct->toAddon->GetMemory(m_dllStruct, memoryType, &gameMemory, &size);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: Exception caught in GetMemory()", m_gameClient->ID());
+ }
+
+ const std::string path = GetPath(memoryType);
+ if (size > 0 && XFILE::CFile::Exists(path))
+ {
+ XFILE::CFile file;
+ if (file.Open(path))
+ {
+ ssize_t read = file.Read(gameMemory, size);
+ if (read == static_cast<ssize_t>(size))
+ {
+ CLog::Log(LOGINFO, "GAME: In-game saves ({}) loaded from {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Failed to read in-game saves ({}): {}/{} bytes read",
+ CGameClientTranslator::ToString(memoryType), read, size);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Unable to open in-game saves ({}) from file {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "GAME: No in-game saves ({}) to load",
+ CGameClientTranslator::ToString(memoryType));
+ }
+}
+
+void CGameClientInGameSaves::Save(GAME_MEMORY memoryType)
+{
+ uint8_t* gameMemory = nullptr;
+ size_t size = 0;
+
+ try
+ {
+ m_dllStruct->toAddon->GetMemory(m_dllStruct, memoryType, &gameMemory, &size);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: Exception caught in GetMemory()", m_gameClient->ID());
+ }
+
+ if (size > 0)
+ {
+ const std::string path = GetPath(memoryType);
+ XFILE::CFile file;
+ if (file.OpenForWrite(path, true))
+ {
+ ssize_t written = 0;
+ written = file.Write(gameMemory, size);
+ file.Close();
+ if (written == static_cast<ssize_t>(size))
+ {
+ CLog::Log(LOGINFO, "GAME: In-game saves ({}) written to {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Failed to write in-game saves ({}): {}/{} bytes written",
+ CGameClientTranslator::ToString(memoryType), written, size);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Unable to open in-game saves ({}) from file {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "GAME: No in-game saves ({}) to save",
+ CGameClientTranslator::ToString(memoryType));
+ }
+}
diff --git a/xbmc/games/addons/GameClientInGameSaves.h b/xbmc/games/addons/GameClientInGameSaves.h
new file mode 100644
index 0000000..a75c155
--- /dev/null
+++ b/xbmc/games/addons/GameClientInGameSaves.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016-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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+
+#include <string>
+
+struct GameClient;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \brief This class implements in-game saves.
+ *
+ * \details Some games do not implement state persistence on their own, but rely on the frontend for
+ * saving their current memory state to disk. This is mostly the case for emulators for SRAM
+ * (battery backed up ram on cartridges) or memory cards.
+ *
+ * Differences to save states:
+ * - Works only for supported games (e.g. emulated games with SRAM support)
+ * - Often works emulator independent (and can be used to start a game with one emulator and
+ * continue with another)
+ * - Visible in-game (e.g. in-game save game selection menus)
+ */
+class CGameClientInGameSaves
+{
+public:
+ /*!
+ * \brief Constructor.
+ * \param addon The game client implementation.
+ * \param dllStruct The emulator or game for which the in-game saves are processed.
+ */
+ CGameClientInGameSaves(CGameClient* addon, const AddonInstance_Game* dllStruct);
+
+ /*!
+ * \brief Load in-game data.
+ */
+ void Load();
+
+ /*!
+ * \brief Save in-game data.
+ */
+ void Save();
+
+private:
+ std::string GetPath(GAME_MEMORY memoryType);
+
+ void Load(GAME_MEMORY memoryType);
+ void Save(GAME_MEMORY memoryType);
+
+ const CGameClient* const m_gameClient;
+ const AddonInstance_Game* const m_dllStruct;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientProperties.cpp b/xbmc/games/addons/GameClientProperties.cpp
new file mode 100644
index 0000000..4f7273a
--- /dev/null
+++ b/xbmc/games/addons/GameClientProperties.cpp
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2012-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 "GameClientProperties.h"
+
+#include "FileItem.h"
+#include "GameClient.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/GameResource.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <cstring>
+
+using namespace KODI;
+using namespace ADDON;
+using namespace GAME;
+using namespace XFILE;
+
+#define GAME_CLIENT_RESOURCES_DIRECTORY "resources"
+
+CGameClientProperties::CGameClientProperties(const CGameClient& parent, AddonProps_Game& props)
+ : m_parent(parent), m_properties(props)
+{
+}
+
+void CGameClientProperties::ReleaseResources(void)
+{
+ for (auto& it : m_proxyDllPaths)
+ delete[] it;
+ m_proxyDllPaths.clear();
+
+ for (auto& it : m_resourceDirectories)
+ delete[] it;
+ m_resourceDirectories.clear();
+
+ for (auto& it : m_extensions)
+ delete[] it;
+ m_extensions.clear();
+}
+
+bool CGameClientProperties::InitializeProperties(void)
+{
+ ReleaseResources();
+
+ ADDON::VECADDONS addons;
+ if (!GetProxyAddons(addons))
+ return false;
+
+ m_properties.game_client_dll_path = GetLibraryPath();
+ m_properties.proxy_dll_paths = GetProxyDllPaths(addons);
+ m_properties.proxy_dll_count = GetProxyDllCount();
+ m_properties.resource_directories = GetResourceDirectories();
+ m_properties.resource_directory_count = GetResourceDirectoryCount();
+ m_properties.profile_directory = GetProfileDirectory();
+ m_properties.supports_vfs = m_parent.SupportsVFS();
+ m_properties.extensions = GetExtensions();
+ m_properties.extension_count = GetExtensionCount();
+
+ return true;
+}
+
+const char* CGameClientProperties::GetLibraryPath(void)
+{
+ if (m_strLibraryPath.empty())
+ {
+ // Get the parent add-on's real path
+ std::string strLibPath = m_parent.CAddonDll::LibPath();
+ m_strLibraryPath = CSpecialProtocol::TranslatePath(strLibPath);
+ URIUtils::RemoveSlashAtEnd(m_strLibraryPath);
+ }
+ return m_strLibraryPath.c_str();
+}
+
+const char** CGameClientProperties::GetProxyDllPaths(const ADDON::VECADDONS& addons)
+{
+ if (m_proxyDllPaths.empty())
+ {
+ for (const auto& addon : addons)
+ AddProxyDll(std::static_pointer_cast<CGameClient>(addon));
+ }
+
+ if (!m_proxyDllPaths.empty())
+ return const_cast<const char**>(m_proxyDllPaths.data());
+
+ return nullptr;
+}
+
+unsigned int CGameClientProperties::GetProxyDllCount(void) const
+{
+ return static_cast<unsigned int>(m_proxyDllPaths.size());
+}
+
+const char** CGameClientProperties::GetResourceDirectories(void)
+{
+ if (m_resourceDirectories.empty())
+ {
+ // Add all other game resources
+ const auto& dependencies = m_parent.GetDependencies();
+ for (auto it = dependencies.begin(); it != dependencies.end(); ++it)
+ {
+ const std::string& strAddonId = it->id;
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(strAddonId, addon, AddonType::RESOURCE_GAMES,
+ OnlyEnabled::CHOICE_YES))
+ {
+ std::shared_ptr<CGameResource> resource = std::static_pointer_cast<CGameResource>(addon);
+
+ std::string resourcePath = resource->GetFullPath("");
+ URIUtils::RemoveSlashAtEnd(resourcePath);
+
+ char* resourceDir = new char[resourcePath.length() + 1];
+ std::strcpy(resourceDir, resourcePath.c_str());
+ m_resourceDirectories.push_back(resourceDir);
+ }
+ }
+
+ // Add resource directories for profile and path
+ std::string addonProfile = CSpecialProtocol::TranslatePath(m_parent.Profile());
+ std::string addonPath = m_parent.Path();
+
+ addonProfile = URIUtils::AddFileToFolder(addonProfile, GAME_CLIENT_RESOURCES_DIRECTORY);
+ addonPath = URIUtils::AddFileToFolder(addonPath, GAME_CLIENT_RESOURCES_DIRECTORY);
+
+ if (!CDirectory::Exists(addonProfile))
+ {
+ CLog::Log(LOGDEBUG, "Creating resource directory: {}", addonProfile);
+ CDirectory::Create(addonProfile);
+ }
+
+ // Only add user profile directory if non-empty
+ CFileItemList items;
+ if (CDirectory::GetDirectory(addonProfile, items, "", DIR_FLAG_DEFAULTS))
+ {
+ if (!items.IsEmpty())
+ {
+ char* addonProfileDir = new char[addonProfile.length() + 1];
+ std::strcpy(addonProfileDir, addonProfile.c_str());
+ m_resourceDirectories.push_back(addonProfileDir);
+ }
+ }
+
+ char* addonPathDir = new char[addonPath.length() + 1];
+ std::strcpy(addonPathDir, addonPath.c_str());
+ m_resourceDirectories.push_back(addonPathDir);
+ }
+
+ if (!m_resourceDirectories.empty())
+ return const_cast<const char**>(m_resourceDirectories.data());
+
+ return nullptr;
+}
+
+unsigned int CGameClientProperties::GetResourceDirectoryCount(void) const
+{
+ return static_cast<unsigned int>(m_resourceDirectories.size());
+}
+
+const char* CGameClientProperties::GetProfileDirectory(void)
+{
+ if (m_strProfileDirectory.empty())
+ {
+ m_strProfileDirectory = CSpecialProtocol::TranslatePath(m_parent.Profile());
+ URIUtils::RemoveSlashAtEnd(m_strProfileDirectory);
+ }
+
+ return m_strProfileDirectory.c_str();
+}
+
+const char** CGameClientProperties::GetExtensions(void)
+{
+ for (auto& extension : m_parent.GetExtensions())
+ {
+ char* ext = new char[extension.length() + 1];
+ std::strcpy(ext, extension.c_str());
+ m_extensions.push_back(ext);
+ }
+
+ return !m_extensions.empty() ? const_cast<const char**>(m_extensions.data()) : nullptr;
+}
+
+unsigned int CGameClientProperties::GetExtensionCount(void) const
+{
+ return static_cast<unsigned int>(m_extensions.size());
+}
+
+bool CGameClientProperties::GetProxyAddons(ADDON::VECADDONS& addons)
+{
+ ADDON::VECADDONS ret;
+ std::vector<std::string> missingDependencies; // ID or name of missing dependencies
+
+ for (const auto& dependency : m_parent.GetDependencies())
+ {
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(dependency.id, addon, OnlyEnabled::CHOICE_NO))
+ {
+ // If add-on is disabled, ask the user to enable it
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(dependency.id))
+ {
+ // "Failed to play game"
+ // "This game depends on a disabled add-on. Would you like to enable it?"
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{35210}, CVariant{35215}))
+ {
+ if (!CServiceBroker::GetAddonMgr().EnableAddon(dependency.id))
+ {
+ CLog::Log(LOGERROR, "Failed to enable add-on {}", dependency.id);
+ missingDependencies.emplace_back(addon->Name());
+ addon.reset();
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "User chose to not enable add-on {}", dependency.id);
+ missingDependencies.emplace_back(addon->Name());
+ addon.reset();
+ }
+ }
+
+ if (addon && addon->Type() == AddonType::GAMEDLL)
+ ret.emplace_back(std::move(addon));
+ }
+ else
+ {
+ if (dependency.optional)
+ {
+ CLog::Log(LOGDEBUG, "Missing optional dependency {}", dependency.id);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Missing mandatory dependency {}", dependency.id);
+ missingDependencies.emplace_back(dependency.id);
+ }
+ }
+ }
+
+ if (!missingDependencies.empty())
+ {
+ std::string strDependencies = StringUtils::Join(missingDependencies, ", ");
+ std::string dialogText = StringUtils::Format(g_localizeStrings.Get(35223), strDependencies);
+
+ // "Failed to play game"
+ // "Add-on is incompatible due to unmet dependencies."
+ // ""
+ // "Missing: {0:s}"
+ MESSAGING::HELPERS::ShowOKDialogLines(CVariant{35210}, CVariant{24104}, CVariant{""},
+ CVariant{dialogText});
+
+ return false;
+ }
+
+ addons = std::move(ret);
+ return true;
+}
+
+void CGameClientProperties::AddProxyDll(const GameClientPtr& gameClient)
+{
+ // Get the add-on's real path
+ std::string strLibPath = gameClient->CAddon::LibPath();
+
+ // Ignore add-on if it is already added
+ if (!HasProxyDll(strLibPath))
+ {
+ char* libPath = new char[strLibPath.length() + 1];
+ std::strcpy(libPath, strLibPath.c_str());
+ m_proxyDllPaths.push_back(libPath);
+ }
+}
+
+bool CGameClientProperties::HasProxyDll(const std::string& strLibPath) const
+{
+ for (const auto& it : m_proxyDllPaths)
+ {
+ if (strLibPath == it)
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/games/addons/GameClientProperties.h b/xbmc/games/addons/GameClientProperties.h
new file mode 100644
index 0000000..5cb1c6a
--- /dev/null
+++ b/xbmc/games/addons/GameClientProperties.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2012-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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/GameTypes.h"
+
+#include <string>
+#include <vector>
+
+struct AddonProps_Game;
+
+namespace ADDON
+{
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+} // namespace ADDON
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameClient;
+
+/**
+ * \ingroup games
+ * \brief C++ wrapper for properties to pass to the DLL
+ *
+ * Game client properties declared in addon-instance/Game.h.
+ */
+class CGameClientProperties
+{
+public:
+ CGameClientProperties(const CGameClient& parent, AddonProps_Game& props);
+ ~CGameClientProperties(void) { ReleaseResources(); }
+
+ bool InitializeProperties(void);
+
+private:
+ // Release mutable resources
+ void ReleaseResources(void);
+
+ // Equal to parent's real library path
+ const char* GetLibraryPath(void);
+
+ // List of proxy DLLs needed to load the game client
+ const char** GetProxyDllPaths(const ADDON::VECADDONS& addons);
+
+ // Number of proxy DLLs needed to load the game client
+ unsigned int GetProxyDllCount(void) const;
+
+ // Paths to game resources
+ const char** GetResourceDirectories(void);
+
+ // Number of resource directories
+ unsigned int GetResourceDirectoryCount(void) const;
+
+ // Equal to special://profile/addon_data/<parent's id>
+ const char* GetProfileDirectory(void);
+
+ // List of extensions from addon.xml
+ const char** GetExtensions(void);
+
+ // Number of extensions
+ unsigned int GetExtensionCount(void) const;
+
+ // Helper functions
+ bool GetProxyAddons(ADDON::VECADDONS& addons);
+ void AddProxyDll(const GameClientPtr& gameClient);
+ bool HasProxyDll(const std::string& strLibPath) const;
+
+ // Construction parameters
+ const CGameClient& m_parent;
+ AddonProps_Game& m_properties;
+
+ // Buffers to hold the strings
+ std::string m_strLibraryPath;
+ std::vector<char*> m_proxyDllPaths;
+ std::vector<char*> m_resourceDirectories;
+ std::string m_strProfileDirectory;
+ std::vector<char*> m_extensions;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientSubsystem.cpp b/xbmc/games/addons/GameClientSubsystem.cpp
new file mode 100644
index 0000000..599125f
--- /dev/null
+++ b/xbmc/games/addons/GameClientSubsystem.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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 "GameClientSubsystem.h"
+
+#include "GameClient.h"
+#include "GameClientProperties.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/cheevos/GameClientCheevos.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/addons/streams/GameClientStreams.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientSubsystem::CGameClientSubsystem(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess)
+ : m_gameClient(gameClient), m_struct(addonStruct), m_clientAccess(clientAccess)
+{
+}
+
+CGameClientSubsystem::~CGameClientSubsystem() = default;
+
+GameClientSubsystems CGameClientSubsystem::CreateSubsystems(CGameClient& gameClient,
+ AddonInstance_Game& gameStruct,
+ CCriticalSection& clientAccess)
+{
+ GameClientSubsystems subsystems = {};
+
+ subsystems.Cheevos = std::make_unique<CGameClientCheevos>(gameClient, gameStruct);
+ subsystems.Input.reset(new CGameClientInput(gameClient, gameStruct, clientAccess));
+ subsystems.AddonProperties.reset(new CGameClientProperties(gameClient, *gameStruct.props));
+ subsystems.Streams.reset(new CGameClientStreams(gameClient));
+
+ return subsystems;
+}
+
+void CGameClientSubsystem::DestroySubsystems(GameClientSubsystems& subsystems)
+{
+ subsystems.Cheevos.reset();
+ subsystems.Input.reset();
+ subsystems.AddonProperties.reset();
+ subsystems.Streams.reset();
+}
+
+CGameClientCheevos& CGameClientSubsystem::Cheevos() const
+{
+ return m_gameClient.Cheevos();
+}
+
+CGameClientInput& CGameClientSubsystem::Input() const
+{
+ return m_gameClient.Input();
+}
+
+CGameClientProperties& CGameClientSubsystem::AddonProperties() const
+{
+ return m_gameClient.AddonProperties();
+}
+
+CGameClientStreams& CGameClientSubsystem::Streams() const
+{
+ return m_gameClient.Streams();
+}
diff --git a/xbmc/games/addons/GameClientSubsystem.h b/xbmc/games/addons/GameClientSubsystem.h
new file mode 100644
index 0000000..4711011
--- /dev/null
+++ b/xbmc/games/addons/GameClientSubsystem.h
@@ -0,0 +1,81 @@
+/*
+ * 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 <memory>
+
+struct AddonInstance_Game;
+class CCriticalSection;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+class CGameClientCheevos;
+class CGameClientInput;
+class CGameClientProperties;
+class CGameClientStreams;
+
+struct GameClientSubsystems
+{
+ std::unique_ptr<CGameClientCheevos> Cheevos;
+ std::unique_ptr<CGameClientInput> Input;
+ std::unique_ptr<CGameClientProperties> AddonProperties;
+ std::unique_ptr<CGameClientStreams> Streams;
+};
+
+/*!
+ * \brief Base class for game client subsystems
+ */
+class CGameClientSubsystem
+{
+protected:
+ CGameClientSubsystem(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess);
+
+ virtual ~CGameClientSubsystem();
+
+public:
+ /*!
+ * \brief Create a struct with the allocated subsystems
+ *
+ * \param gameClient The owner of the subsystems
+ * \param gameStruct The game client's add-on function table
+ * \param clientAccess Mutex guarding client function access
+ *
+ * \return A fully-allocated GameClientSubsystems struct
+ */
+ static GameClientSubsystems CreateSubsystems(CGameClient& gameClient,
+ AddonInstance_Game& gameStruct,
+ CCriticalSection& clientAccess);
+
+ /*!
+ * \brief Deallocate subsystems
+ *
+ * \param subsystems The subsystems created by CreateSubsystems()
+ */
+ static void DestroySubsystems(GameClientSubsystems& subsystems);
+
+protected:
+ // Subsystems
+ CGameClientCheevos& Cheevos() const;
+ CGameClientInput& Input() const;
+ CGameClientProperties& AddonProperties() const;
+ CGameClientStreams& Streams() const;
+
+ // Construction parameters
+ CGameClient& m_gameClient;
+ AddonInstance_Game& m_struct;
+ CCriticalSection& m_clientAccess;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientTranslator.cpp b/xbmc/games/addons/GameClientTranslator.cpp
new file mode 100644
index 0000000..c48ba4a
--- /dev/null
+++ b/xbmc/games/addons/GameClientTranslator.cpp
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2016-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 "GameClientTranslator.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const char* CGameClientTranslator::ToString(GAME_ERROR error)
+{
+ switch (error)
+ {
+ case GAME_ERROR_NO_ERROR:
+ return "no error";
+ case GAME_ERROR_NOT_IMPLEMENTED:
+ return "not implemented";
+ case GAME_ERROR_REJECTED:
+ return "rejected by the client";
+ case GAME_ERROR_INVALID_PARAMETERS:
+ return "invalid parameters for this method";
+ case GAME_ERROR_FAILED:
+ return "the command failed";
+ case GAME_ERROR_NOT_LOADED:
+ return "no game is loaded";
+ case GAME_ERROR_RESTRICTED:
+ return "the required resources are restricted";
+ default:
+ break;
+ }
+ return "unknown error";
+}
+
+const char* CGameClientTranslator::ToString(GAME_MEMORY memory)
+{
+ switch (memory)
+ {
+ case GAME_MEMORY_SAVE_RAM:
+ return "save ram";
+ case GAME_MEMORY_RTC:
+ return "rtc";
+ case GAME_MEMORY_SYSTEM_RAM:
+ return "system ram";
+ case GAME_MEMORY_VIDEO_RAM:
+ return "video ram";
+ case GAME_MEMORY_SNES_BSX_RAM:
+ return "snes bsx ram";
+ case GAME_MEMORY_SNES_SUFAMI_TURBO_A_RAM:
+ return "snes sufami turbo a ram";
+ case GAME_MEMORY_SNES_SUFAMI_TURBO_B_RAM:
+ return "snes sufami turbo b ram";
+ case GAME_MEMORY_SNES_GAME_BOY_RAM:
+ return "snes game boy ram";
+ case GAME_MEMORY_SNES_GAME_BOY_RTC:
+ return "snes game boy rtc";
+ default:
+ break;
+ }
+ return "unknown memory";
+}
+
+bool CGameClientTranslator::TranslateStreamType(GAME_STREAM_TYPE gameType,
+ RETRO::StreamType& retroType)
+{
+ switch (gameType)
+ {
+ case GAME_STREAM_AUDIO:
+ retroType = RETRO::StreamType::AUDIO;
+ return true;
+ case GAME_STREAM_VIDEO:
+ retroType = RETRO::StreamType::VIDEO;
+ return true;
+ case GAME_STREAM_SW_FRAMEBUFFER:
+ retroType = RETRO::StreamType::SW_BUFFER;
+ return true;
+ case GAME_STREAM_HW_FRAMEBUFFER:
+ retroType = RETRO::StreamType::HW_BUFFER;
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+AVPixelFormat CGameClientTranslator::TranslatePixelFormat(GAME_PIXEL_FORMAT format)
+{
+ switch (format)
+ {
+ case GAME_PIXEL_FORMAT_0RGB8888:
+ return AV_PIX_FMT_0RGB32;
+ case GAME_PIXEL_FORMAT_RGB565:
+ return AV_PIX_FMT_RGB565;
+ case GAME_PIXEL_FORMAT_0RGB1555:
+ return AV_PIX_FMT_RGB555;
+ default:
+ break;
+ }
+ return AV_PIX_FMT_NONE;
+}
+
+GAME_PIXEL_FORMAT CGameClientTranslator::TranslatePixelFormat(AVPixelFormat format)
+{
+ switch (format)
+ {
+ case AV_PIX_FMT_0RGB32:
+ return GAME_PIXEL_FORMAT_0RGB8888;
+ case AV_PIX_FMT_RGB565:
+ return GAME_PIXEL_FORMAT_RGB565;
+ case AV_PIX_FMT_RGB555:
+ return GAME_PIXEL_FORMAT_0RGB1555;
+ default:
+ break;
+ }
+ return GAME_PIXEL_FORMAT_UNKNOWN;
+}
+
+RETRO::PCMFormat CGameClientTranslator::TranslatePCMFormat(GAME_PCM_FORMAT format)
+{
+ switch (format)
+ {
+ case GAME_PCM_FORMAT_S16NE:
+ return RETRO::PCMFormat::FMT_S16NE;
+ default:
+ break;
+ }
+ return RETRO::PCMFormat::FMT_UNKNOWN;
+}
+
+RETRO::AudioChannel CGameClientTranslator::TranslateAudioChannel(GAME_AUDIO_CHANNEL channel)
+{
+ switch (channel)
+ {
+ case GAME_CH_FL:
+ return RETRO::AudioChannel::CH_FL;
+ case GAME_CH_FR:
+ return RETRO::AudioChannel::CH_FR;
+ case GAME_CH_FC:
+ return RETRO::AudioChannel::CH_FC;
+ case GAME_CH_LFE:
+ return RETRO::AudioChannel::CH_LFE;
+ case GAME_CH_BL:
+ return RETRO::AudioChannel::CH_BL;
+ case GAME_CH_BR:
+ return RETRO::AudioChannel::CH_BR;
+ case GAME_CH_FLOC:
+ return RETRO::AudioChannel::CH_FLOC;
+ case GAME_CH_FROC:
+ return RETRO::AudioChannel::CH_FROC;
+ case GAME_CH_BC:
+ return RETRO::AudioChannel::CH_BC;
+ case GAME_CH_SL:
+ return RETRO::AudioChannel::CH_SL;
+ case GAME_CH_SR:
+ return RETRO::AudioChannel::CH_SR;
+ case GAME_CH_TFL:
+ return RETRO::AudioChannel::CH_TFL;
+ case GAME_CH_TFR:
+ return RETRO::AudioChannel::CH_TFR;
+ case GAME_CH_TFC:
+ return RETRO::AudioChannel::CH_TFC;
+ case GAME_CH_TC:
+ return RETRO::AudioChannel::CH_TC;
+ case GAME_CH_TBL:
+ return RETRO::AudioChannel::CH_TBL;
+ case GAME_CH_TBR:
+ return RETRO::AudioChannel::CH_TBR;
+ case GAME_CH_TBC:
+ return RETRO::AudioChannel::CH_TBC;
+ case GAME_CH_BLOC:
+ return RETRO::AudioChannel::CH_BLOC;
+ case GAME_CH_BROC:
+ return RETRO::AudioChannel::CH_BROC;
+ default:
+ break;
+ }
+ return RETRO::AudioChannel::CH_NULL;
+}
+
+RETRO::VideoRotation CGameClientTranslator::TranslateRotation(GAME_VIDEO_ROTATION rotation)
+{
+ switch (rotation)
+ {
+ case GAME_VIDEO_ROTATION_90_CCW:
+ return RETRO::VideoRotation::ROTATION_90_CCW;
+ case GAME_VIDEO_ROTATION_180_CCW:
+ return RETRO::VideoRotation::ROTATION_180_CCW;
+ case GAME_VIDEO_ROTATION_270_CCW:
+ return RETRO::VideoRotation::ROTATION_270_CCW;
+ default:
+ break;
+ }
+ return RETRO::VideoRotation::ROTATION_0;
+}
+
+GAME_KEY_MOD CGameClientTranslator::GetModifiers(KEYBOARD::Modifier modifier)
+{
+ using namespace KEYBOARD;
+
+ unsigned int mods = GAME_KEY_MOD_NONE;
+
+ if (modifier & Modifier::MODIFIER_CTRL)
+ mods |= GAME_KEY_MOD_CTRL;
+ if (modifier & Modifier::MODIFIER_SHIFT)
+ mods |= GAME_KEY_MOD_SHIFT;
+ if (modifier & Modifier::MODIFIER_ALT)
+ mods |= GAME_KEY_MOD_ALT;
+ if (modifier & Modifier::MODIFIER_RALT)
+ mods |= GAME_KEY_MOD_ALT;
+ if (modifier & Modifier::MODIFIER_META)
+ mods |= GAME_KEY_MOD_META;
+ if (modifier & Modifier::MODIFIER_SUPER)
+ mods |= GAME_KEY_MOD_SUPER;
+ if (modifier & Modifier::MODIFIER_NUMLOCK)
+ mods |= GAME_KEY_MOD_NUMLOCK;
+ if (modifier & Modifier::MODIFIER_CAPSLOCK)
+ mods |= GAME_KEY_MOD_CAPSLOCK;
+ if (modifier & Modifier::MODIFIER_SCROLLLOCK)
+ mods |= GAME_KEY_MOD_SCROLLOCK;
+
+ return static_cast<GAME_KEY_MOD>(mods);
+}
+
+const char* CGameClientTranslator::TranslateRegion(GAME_REGION region)
+{
+ switch (region)
+ {
+ case GAME_REGION_NTSC:
+ return "NTSC";
+ case GAME_REGION_PAL:
+ return "PAL";
+ default:
+ break;
+ }
+ return "Unknown";
+}
+
+PORT_TYPE CGameClientTranslator::TranslatePortType(GAME_PORT_TYPE portType)
+{
+ switch (portType)
+ {
+ case GAME_PORT_KEYBOARD:
+ return PORT_TYPE::KEYBOARD;
+ case GAME_PORT_MOUSE:
+ return PORT_TYPE::MOUSE;
+ case GAME_PORT_CONTROLLER:
+ return PORT_TYPE::CONTROLLER;
+ default:
+ break;
+ }
+
+ return PORT_TYPE::UNKNOWN;
+}
diff --git a/xbmc/games/addons/GameClientTranslator.h b/xbmc/games/addons/GameClientTranslator.h
new file mode 100644
index 0000000..6b6b448
--- /dev/null
+++ b/xbmc/games/addons/GameClientTranslator.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016-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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "cores/RetroPlayer/streams/RetroPlayerStreamTypes.h"
+#include "games/controllers/ControllerTypes.h"
+#include "input/keyboard/KeyboardTypes.h"
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \ingroup games
+ * \brief Translates data types from Game API to the corresponding format in Kodi.
+ *
+ * This class is stateless.
+ */
+class CGameClientTranslator
+{
+ CGameClientTranslator() = delete;
+
+public:
+ /*!
+ * \brief Translates game errors to string representation (e.g. for logging).
+ * \param error The error to translate.
+ * \return Translated error.
+ */
+ static const char* ToString(GAME_ERROR error);
+
+ /*!
+ * \brief Translates game memory types to string representation (e.g. for logging).
+ * \param memory The memory type to translate.
+ * \return Translated memory type.
+ */
+ static const char* ToString(GAME_MEMORY error);
+
+ /*!
+ * \brief Translate stream type (Game API to RetroPlayer).
+ * \param gameType The stream type to translate.
+ * \param[out] retroType The translated stream type.
+ * \return True if the Game API type was translated to a valid RetroPlayer type
+ */
+ static bool TranslateStreamType(GAME_STREAM_TYPE gameType, RETRO::StreamType& retroType);
+
+ /*!
+ * \brief Translate pixel format (Game API to RetroPlayer/FFMPEG).
+ * \param format The pixel format to translate.
+ * \return Translated pixel format.
+ */
+ static AVPixelFormat TranslatePixelFormat(GAME_PIXEL_FORMAT format);
+
+ /*!
+ * \brief Translate pixel format (RetroPlayer/FFMPEG to Game API).
+ * \param format The pixel format to translate.
+ * \return Translated pixel format.
+ */
+ static GAME_PIXEL_FORMAT TranslatePixelFormat(AVPixelFormat format);
+
+ /*!
+ * \brief Translate audio PCM format (Game API to RetroPlayer).
+ * \param format The audio PCM format to translate.
+ * \return Translated audio PCM format.
+ */
+ static RETRO::PCMFormat TranslatePCMFormat(GAME_PCM_FORMAT format);
+
+ /*!
+ * \brief Translate audio channels (Game API to RetroPlayer).
+ * \param format The audio channels to translate.
+ * \return Translated audio channels.
+ */
+ static RETRO::AudioChannel TranslateAudioChannel(GAME_AUDIO_CHANNEL channel);
+
+ /*!
+ * \brief Translate video rotation (Game API to RetroPlayer).
+ * \param rotation The video rotation to translate.
+ * \return Translated video rotation.
+ */
+ static RETRO::VideoRotation TranslateRotation(GAME_VIDEO_ROTATION rotation);
+
+ /*!
+ * \brief Translate key modifiers (Kodi to Game API).
+ * \param modifiers The key modifiers to translate (e.g. Shift, Ctrl).
+ * \return Translated key modifiers.
+ */
+ static GAME_KEY_MOD GetModifiers(KEYBOARD::Modifier modifier);
+
+ /*!
+ * \brief Translate region to string representation (e.g. for logging).
+ * \param error The region to translate (e.g. PAL, NTSC).
+ * \return Translated region.
+ */
+ static const char* TranslateRegion(GAME_REGION region);
+
+ /*!
+ * \brief Translate port type (Game API to Kodi)
+ * \param portType The port type to translate
+ * \return Translated port type
+ */
+ static PORT_TYPE TranslatePortType(GAME_PORT_TYPE portType);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/cheevos/CMakeLists.txt b/xbmc/games/addons/cheevos/CMakeLists.txt
new file mode 100644
index 0000000..826fc04
--- /dev/null
+++ b/xbmc/games/addons/cheevos/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES GameClientCheevos.cpp)
+
+set(HEADERS GameClientCheevos.h)
+
+core_add_library(gamecheevos)
diff --git a/xbmc/games/addons/cheevos/GameClientCheevos.cpp b/xbmc/games/addons/cheevos/GameClientCheevos.cpp
new file mode 100644
index 0000000..e9d457b
--- /dev/null
+++ b/xbmc/games/addons/cheevos/GameClientCheevos.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2020-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 "GameClientCheevos.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h"
+#include "cores/RetroPlayer/cheevos/RConsoleIDs.h"
+#include "games/addons/GameClient.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientCheevos::CGameClientCheevos(CGameClient& gameClient, AddonInstance_Game& addonStruct)
+ : m_gameClient(gameClient), m_struct(addonStruct)
+{
+}
+
+bool CGameClientCheevos::RCGenerateHashFromFile(std::string& hash,
+ RETRO::RConsoleID consoleID,
+ const std::string& filePath)
+{
+ char* _hash = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(
+ error = m_struct.toAddon->RCGenerateHashFromFile(
+ &m_struct, &_hash, static_cast<unsigned int>(consoleID), filePath.c_str()),
+ "RCGenerateHashFromFile()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetGameIDUrl()");
+ }
+
+ if (_hash)
+ {
+ hash = _hash;
+ m_struct.toAddon->FreeString(&m_struct, _hash);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+bool CGameClientCheevos::RCGetGameIDUrl(std::string& url, const std::string& hash)
+{
+ char* _url = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCGetGameIDUrl(&m_struct, &_url, hash.c_str()),
+ "RCGetGameIDUrl()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetGameIDUrl()");
+ }
+
+ if (_url)
+ {
+ url = _url;
+ m_struct.toAddon->FreeString(&m_struct, _url);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+bool CGameClientCheevos::RCGetPatchFileUrl(std::string& url,
+ const std::string& username,
+ const std::string& token,
+ unsigned int gameID)
+{
+ char* _url = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCGetPatchFileUrl(
+ &m_struct, &_url, username.c_str(), token.c_str(), gameID),
+ "RCGetPatchFileUrl()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetPatchFileUrl()");
+ }
+
+ if (_url)
+ {
+ url = _url;
+ m_struct.toAddon->FreeString(&m_struct, _url);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+bool CGameClientCheevos::RCPostRichPresenceUrl(std::string& url,
+ std::string& postData,
+ const std::string& username,
+ const std::string& token,
+ unsigned gameID,
+ const std::string& richPresence)
+{
+ char* _url = nullptr;
+ char* _postData = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCPostRichPresenceUrl(
+ &m_struct, &_url, &_postData, username.c_str(), token.c_str(), gameID,
+ richPresence.c_str()),
+ "RCPostRichPresenceUrl()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCPostRichPresenceUrl()");
+ }
+
+ if (_url)
+ {
+ url = _url;
+ m_struct.toAddon->FreeString(&m_struct, _url);
+ }
+ if (_postData)
+ {
+ postData = _postData;
+ m_struct.toAddon->FreeString(&m_struct, _postData);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+void CGameClientCheevos::RCEnableRichPresence(const std::string& script)
+{
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCEnableRichPresence(&m_struct, script.c_str()),
+ "RCEnableRichPresence()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCEnableRichPresence()");
+ }
+}
+
+void CGameClientCheevos::RCGetRichPresenceEvaluation(std::string& evaluation,
+ RETRO::RConsoleID consoleID)
+{
+ char* _evaluation = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCGetRichPresenceEvaluation(
+ &m_struct, &_evaluation, static_cast<unsigned int>(consoleID)),
+ "RCGetRichPresenceEvaluation()");
+
+ if (_evaluation)
+ {
+ evaluation = _evaluation;
+ m_struct.toAddon->FreeString(&m_struct, _evaluation);
+ }
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetRichPresenceEvaluation()");
+ }
+}
+
+void CGameClientCheevos::RCResetRuntime()
+{
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCResetRuntime(&m_struct), "RCResetRuntime()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCResetRuntime()");
+ }
+}
diff --git a/xbmc/games/addons/cheevos/GameClientCheevos.h b/xbmc/games/addons/cheevos/GameClientCheevos.h
new file mode 100644
index 0000000..2be8c7a
--- /dev/null
+++ b/xbmc/games/addons/cheevos/GameClientCheevos.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020-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 <stddef.h> /* size_t */
+#include <string>
+
+struct AddonInstance_Game;
+
+namespace KODI
+{
+namespace RETRO
+{
+enum class RConsoleID;
+}
+
+namespace GAME
+{
+
+class CGameClient;
+
+class CGameClientCheevos
+{
+public:
+ CGameClientCheevos(CGameClient& gameClient, AddonInstance_Game& addonStruct);
+
+ bool RCGenerateHashFromFile(std::string& hash,
+ RETRO::RConsoleID consoleID,
+ const std::string& filePath);
+ bool RCGetGameIDUrl(std::string& url, const std::string& hash);
+ bool RCGetPatchFileUrl(std::string& url,
+ const std::string& username,
+ const std::string& token,
+ unsigned int gameID);
+ bool RCPostRichPresenceUrl(std::string& url,
+ std::string& postData,
+ const std::string& username,
+ const std::string& token,
+ unsigned gameID,
+ const std::string& richPresence);
+ void RCEnableRichPresence(const std::string& script);
+ void RCGetRichPresenceEvaluation(std::string& evaluation, RETRO::RConsoleID consoleID);
+ // When the game is reset, the runtime should also be reset
+ void RCResetRuntime();
+
+private:
+ CGameClient& m_gameClient;
+ AddonInstance_Game& m_struct;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/CMakeLists.txt b/xbmc/games/addons/input/CMakeLists.txt
new file mode 100644
index 0000000..69cc342
--- /dev/null
+++ b/xbmc/games/addons/input/CMakeLists.txt
@@ -0,0 +1,23 @@
+set(SOURCES GameClientController.cpp
+ GameClientDevice.cpp
+ GameClientHardware.cpp
+ GameClientInput.cpp
+ GameClientJoystick.cpp
+ GameClientKeyboard.cpp
+ GameClientMouse.cpp
+ GameClientPort.cpp
+ GameClientTopology.cpp
+)
+
+set(HEADERS GameClientController.h
+ GameClientDevice.h
+ GameClientHardware.h
+ GameClientInput.h
+ GameClientJoystick.h
+ GameClientKeyboard.h
+ GameClientMouse.h
+ GameClientPort.h
+ GameClientTopology.h
+)
+
+core_add_library(gameinput)
diff --git a/xbmc/games/addons/input/GameClientController.cpp b/xbmc/games/addons/input/GameClientController.cpp
new file mode 100644
index 0000000..435e277
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientController.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2018 Team Kodi
+ * http://kodi.tv
+ *
+ * This Program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This Program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this Program; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "GameClientController.h"
+
+#include "GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "games/controllers/input/PhysicalTopology.h"
+
+#include <algorithm>
+#include <vector>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientController::CGameClientController(CGameClientInput& input, ControllerPtr controller)
+ : m_input(input), m_controller(std::move(controller)), m_controllerId(m_controller->ID())
+{
+ // Generate arrays of features
+ for (const CPhysicalFeature& feature : m_controller->Features())
+ {
+ // Skip feature if not supported by the game client
+ if (!m_input.HasFeature(m_controller->ID(), feature.Name()))
+ continue;
+
+ // Add feature to array of the appropriate type
+ switch (feature.Type())
+ {
+ case FEATURE_TYPE::SCALAR:
+ {
+ switch (feature.InputType())
+ {
+ case JOYSTICK::INPUT_TYPE::DIGITAL:
+ m_digitalButtons.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case JOYSTICK::INPUT_TYPE::ANALOG:
+ m_analogButtons.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case FEATURE_TYPE::ANALOG_STICK:
+ m_analogSticks.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::ACCELEROMETER:
+ m_accelerometers.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::KEY:
+ m_keys.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::RELPOINTER:
+ m_relPointers.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::ABSPOINTER:
+ m_absPointers.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::MOTOR:
+ m_motors.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ default:
+ break;
+ }
+ }
+
+ //! @todo Sort vectors
+}
+
+game_controller_layout CGameClientController::TranslateController() const
+{
+ game_controller_layout controllerStruct{};
+
+ controllerStruct.controller_id = const_cast<char*>(m_controllerId.c_str());
+ controllerStruct.provides_input = m_controller->Layout().Topology().ProvidesInput();
+
+ if (!m_digitalButtons.empty())
+ {
+ controllerStruct.digital_buttons = const_cast<char**>(m_digitalButtons.data());
+ controllerStruct.digital_button_count = static_cast<unsigned int>(m_digitalButtons.size());
+ }
+ if (!m_analogButtons.empty())
+ {
+ controllerStruct.analog_buttons = const_cast<char**>(m_analogButtons.data());
+ controllerStruct.analog_button_count = static_cast<unsigned int>(m_analogButtons.size());
+ }
+ if (!m_analogSticks.empty())
+ {
+ controllerStruct.analog_sticks = const_cast<char**>(m_analogSticks.data());
+ controllerStruct.analog_stick_count = static_cast<unsigned int>(m_analogSticks.size());
+ }
+ if (!m_accelerometers.empty())
+ {
+ controllerStruct.accelerometers = const_cast<char**>(m_accelerometers.data());
+ controllerStruct.accelerometer_count = static_cast<unsigned int>(m_accelerometers.size());
+ }
+ if (!m_keys.empty())
+ {
+ controllerStruct.keys = const_cast<char**>(m_keys.data());
+ controllerStruct.key_count = static_cast<unsigned int>(m_keys.size());
+ }
+ if (!m_relPointers.empty())
+ {
+ controllerStruct.rel_pointers = const_cast<char**>(m_relPointers.data());
+ controllerStruct.rel_pointer_count = static_cast<unsigned int>(m_relPointers.size());
+ }
+ if (!m_absPointers.empty())
+ {
+ controllerStruct.abs_pointers = const_cast<char**>(m_absPointers.data());
+ controllerStruct.abs_pointer_count = static_cast<unsigned int>(m_absPointers.size());
+ }
+ if (!m_motors.empty())
+ {
+ controllerStruct.motors = const_cast<char**>(m_motors.data());
+ controllerStruct.motor_count = static_cast<unsigned int>(m_motors.size());
+ }
+
+ return controllerStruct;
+}
diff --git a/xbmc/games/addons/input/GameClientController.h b/xbmc/games/addons/input/GameClientController.h
new file mode 100644
index 0000000..5445868
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientController.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 Team Kodi
+ * http://kodi.tv
+ *
+ * This Program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This Program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this Program; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClientInput;
+
+/*!
+ * \brief A container for the layout of a controller connected to a game
+ * client input port
+ */
+class CGameClientController
+{
+public:
+ /*!
+ * \brief Construct a controller layout
+ *
+ * \brief controller The controller add-on
+ */
+ CGameClientController(CGameClientInput& input, ControllerPtr controller);
+
+ /*!
+ * \brief Get a controller layout for the Game API
+ */
+ game_controller_layout TranslateController() const;
+
+private:
+ // Construction parameters
+ CGameClientInput& m_input;
+ const ControllerPtr m_controller;
+
+ // Buffer parameters
+ std::string m_controllerId;
+ std::vector<char*> m_digitalButtons;
+ std::vector<char*> m_analogButtons;
+ std::vector<char*> m_analogSticks;
+ std::vector<char*> m_accelerometers;
+ std::vector<char*> m_keys;
+ std::vector<char*> m_relPointers;
+ std::vector<char*> m_absPointers;
+ std::vector<char*> m_motors;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientDevice.cpp b/xbmc/games/addons/input/GameClientDevice.cpp
new file mode 100644
index 0000000..589fb66
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientDevice.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "GameClientDevice.h"
+
+#include "GameClientPort.h"
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/GameServices.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientDevice::CGameClientDevice(const game_input_device& device)
+ : m_controller(GetController(device.controller_id))
+{
+ if (m_controller && device.available_ports != nullptr)
+ {
+ // Look for matching ports. We enumerate in physical order because logical
+ // order can change per emulator.
+ for (const auto& physicalPort : m_controller->Topology().Ports())
+ {
+ for (unsigned int i = 0; i < device.port_count; i++)
+ {
+ const auto& logicalPort = device.available_ports[i];
+ if (logicalPort.port_id != nullptr && logicalPort.port_id == physicalPort.ID())
+ {
+ // Handle matching ports
+ AddPort(logicalPort, physicalPort);
+ break;
+ }
+ }
+ }
+ }
+}
+
+CGameClientDevice::CGameClientDevice(const ControllerPtr& controller) : m_controller(controller)
+{
+}
+
+CGameClientDevice::~CGameClientDevice() = default;
+
+void CGameClientDevice::AddPort(const game_input_port& logicalPort,
+ const CPhysicalPort& physicalPort)
+{
+ std::unique_ptr<CGameClientPort> port(new CGameClientPort(logicalPort, physicalPort));
+ m_ports.emplace_back(std::move(port));
+}
+
+ControllerPtr CGameClientDevice::GetController(const char* controllerId)
+{
+ ControllerPtr controller;
+
+ if (controllerId != nullptr)
+ {
+ controller = CServiceBroker::GetGameServices().GetController(controllerId);
+ if (!controller)
+ CLog::Log(LOGERROR, "Invalid controller ID: {}", controllerId);
+ }
+
+ return controller;
+}
diff --git a/xbmc/games/addons/input/GameClientDevice.h b/xbmc/games/addons/input/GameClientDevice.h
new file mode 100644
index 0000000..403d76c
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientDevice.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 "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+
+struct game_input_device;
+struct game_input_port;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalPort;
+
+/*!
+ * \ingroup games
+ * \brief Represents a device connected to a port
+ */
+class CGameClientDevice
+{
+public:
+ /*!
+ * \brief Construct a device
+ *
+ * \param device The device Game API struct
+ */
+ CGameClientDevice(const game_input_device& device);
+
+ /*!
+ * \brief Construct a device from a controller add-on
+ *
+ * \param controller The controller add-on
+ */
+ CGameClientDevice(const ControllerPtr& controller);
+
+ /*!
+ * \brief Destructor
+ */
+ ~CGameClientDevice();
+
+ /*!
+ * \brief The controller profile
+ */
+ const ControllerPtr& Controller() const { return m_controller; }
+
+ /*!
+ * \brief The ports on this device
+ */
+ const GameClientPortVec& Ports() const { return m_ports; }
+
+private:
+ /*!
+ * \brief Add a controller port
+ *
+ * \param logicalPort The logical port Game API struct
+ * \param physicalPort The physical port definition
+ */
+ void AddPort(const game_input_port& logicalPort, const CPhysicalPort& physicalPort);
+
+ // Helper function
+ static ControllerPtr GetController(const char* controllerId);
+
+ ControllerPtr m_controller;
+ GameClientPortVec m_ports;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientHardware.cpp b/xbmc/games/addons/input/GameClientHardware.cpp
new file mode 100644
index 0000000..bacff90
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientHardware.cpp
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015-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 "GameClientHardware.h"
+
+#include "games/addons/GameClient.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientHardware::CGameClientHardware(CGameClient& gameClient) : m_gameClient(gameClient)
+{
+}
+
+void CGameClientHardware::OnResetButton()
+{
+ CLog::Log(LOGDEBUG, "{}: Sending hardware reset", m_gameClient.ID());
+ m_gameClient.Reset();
+}
diff --git a/xbmc/games/addons/input/GameClientHardware.h b/xbmc/games/addons/input/GameClientHardware.h
new file mode 100644
index 0000000..1ffc96b
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientHardware.h
@@ -0,0 +1,43 @@
+/*
+ * 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/hardware/IHardwareInput.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \ingroup games
+ * \brief Handles events for hardware such as reset buttons
+ */
+class CGameClientHardware : public HARDWARE::IHardwareInput
+{
+public:
+ /*!
+ * \brief Constructor
+ *
+ * \param gameClient The game client implementation
+ */
+ explicit CGameClientHardware(CGameClient& gameClient);
+
+ ~CGameClientHardware() override = default;
+
+ // Implementation of IHardwareInput
+ void OnResetButton() override;
+
+private:
+ // Construction parameter
+ CGameClient& m_gameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientInput.cpp b/xbmc/games/addons/input/GameClientInput.cpp
new file mode 100644
index 0000000..5805cbf
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientInput.cpp
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GameClientInput.h"
+
+#include "GameClientController.h"
+#include "GameClientHardware.h"
+#include "GameClientJoystick.h"
+#include "GameClientKeyboard.h"
+#include "GameClientMouse.h"
+#include "GameClientPort.h"
+#include "GameClientTopology.h"
+#include "ServiceBroker.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/GameServices.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/GameClientCallbacks.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/controllers/types/ControllerNode.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "games/ports/input/PortManager.h"
+#include "games/ports/types/PortNode.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "peripherals/EventLockHandle.h"
+#include "peripherals/Peripherals.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientInput::CGameClientInput(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess)
+ : CGameClientSubsystem(gameClient, addonStruct, clientAccess),
+ m_topology(new CGameClientTopology),
+ m_portManager(std::make_unique<CPortManager>())
+{
+}
+
+CGameClientInput::~CGameClientInput()
+{
+ Deinitialize();
+}
+
+void CGameClientInput::Initialize()
+{
+ LoadTopology();
+
+ // Send controller layouts to game client
+ SetControllerLayouts(m_topology->GetControllerTree().GetControllers());
+
+ // Reset ports to default state (first accepted controller is connected)
+ ActivateControllers(m_topology->GetControllerTree());
+
+ // Initialize the port manager
+ m_portManager->Initialize(m_gameClient.Profile());
+ m_portManager->SetControllerTree(m_topology->GetControllerTree());
+ m_portManager->LoadXML();
+}
+
+void CGameClientInput::Start(IGameInputCallback* input)
+{
+ m_inputCallback = input;
+
+ // Connect/disconnect active controllers
+ for (const CPortNode& port : GetActiveControllerTree().GetPorts())
+ {
+ if (port.IsConnected())
+ {
+ const ControllerPtr& activeController = port.GetActiveController().GetController();
+ if (activeController)
+ ConnectController(port.GetAddress(), activeController);
+ }
+ else
+ DisconnectController(port.GetAddress());
+ }
+
+ // Ensure hardware is open to receive events
+ m_hardware.reset(new CGameClientHardware(m_gameClient));
+
+ // Notify observers of the initial port configuration
+ NotifyObservers(ObservableMessageGamePortsChanged);
+}
+
+void CGameClientInput::Deinitialize()
+{
+ Stop();
+
+ m_topology->Clear();
+ m_controllerLayouts.clear();
+ m_portManager->Clear();
+}
+
+void CGameClientInput::Stop()
+{
+ m_hardware.reset();
+
+ CloseMouse();
+
+ CloseKeyboard();
+
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+ CloseJoysticks(inputHandlingLock);
+
+ // If a port was closed, then this blocks until all peripheral input has
+ // been handled
+ inputHandlingLock.reset();
+
+ m_inputCallback = nullptr;
+}
+
+bool CGameClientInput::HasFeature(const std::string& controllerId,
+ const std::string& featureName) const
+{
+ bool bHasFeature = false;
+
+ try
+ {
+ bHasFeature =
+ m_struct.toAddon->HasFeature(&m_struct, controllerId.c_str(), featureName.c_str());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: exception caught in HasFeature()", m_gameClient.ID());
+
+ // Fail gracefully
+ bHasFeature = true;
+ }
+
+ return bHasFeature;
+}
+
+bool CGameClientInput::AcceptsInput() const
+{
+ if (m_inputCallback != nullptr)
+ return m_inputCallback->AcceptsInput();
+
+ return false;
+}
+
+bool CGameClientInput::InputEvent(const game_input_event& event)
+{
+ bool bHandled = false;
+
+ try
+ {
+ bHandled = m_struct.toAddon->InputEvent(&m_struct, &event);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: exception caught in InputEvent()", m_gameClient.ID());
+ }
+
+ return bHandled;
+}
+
+void CGameClientInput::LoadTopology()
+{
+ game_input_topology* topologyStruct = nullptr;
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ topologyStruct = m_struct.toAddon->GetTopology(&m_struct);
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("GetTopology()");
+ }
+ }
+
+ GameClientPortVec hardwarePorts;
+ int playerLimit = -1;
+
+ if (topologyStruct != nullptr)
+ {
+ //! @todo Guard against infinite loops provided by the game client
+
+ game_input_port* ports = topologyStruct->ports;
+ if (ports != nullptr)
+ {
+ for (unsigned int i = 0; i < topologyStruct->port_count; i++)
+ hardwarePorts.emplace_back(new CGameClientPort(ports[i]));
+ }
+
+ playerLimit = topologyStruct->player_limit;
+
+ try
+ {
+ m_struct.toAddon->FreeTopology(&m_struct, topologyStruct);
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("FreeTopology()");
+ }
+ }
+
+ // If no topology is available, create a default one with a single port that
+ // accepts all controllers imported by addon.xml
+ if (hardwarePorts.empty())
+ hardwarePorts.emplace_back(new CGameClientPort(GetControllers(m_gameClient)));
+
+ m_topology.reset(new CGameClientTopology(std::move(hardwarePorts), playerLimit));
+}
+
+void CGameClientInput::ActivateControllers(CControllerHub& hub)
+{
+ for (auto& port : hub.GetPorts())
+ {
+ if (port.GetCompatibleControllers().empty())
+ continue;
+
+ port.SetConnected(true);
+ port.SetActiveController(0);
+ for (auto& controller : port.GetCompatibleControllers())
+ ActivateControllers(controller.GetHub());
+ }
+}
+
+void CGameClientInput::SetControllerLayouts(const ControllerVector& controllers)
+{
+ if (controllers.empty())
+ return;
+
+ for (const auto& controller : controllers)
+ {
+ const std::string controllerId = controller->ID();
+ if (m_controllerLayouts.find(controllerId) == m_controllerLayouts.end())
+ m_controllerLayouts[controllerId].reset(new CGameClientController(*this, controller));
+ }
+
+ std::vector<game_controller_layout> controllerStructs;
+ for (const auto& it : m_controllerLayouts)
+ controllerStructs.emplace_back(it.second->TranslateController());
+
+ try
+ {
+ m_struct.toAddon->SetControllerLayouts(&m_struct, controllerStructs.data(),
+ static_cast<unsigned int>(controllerStructs.size()));
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("SetControllerLayouts()");
+ }
+}
+
+const CControllerTree& CGameClientInput::GetDefaultControllerTree() const
+{
+ return m_topology->GetControllerTree();
+}
+
+const CControllerTree& CGameClientInput::GetActiveControllerTree() const
+{
+ return m_portManager->GetControllerTree();
+}
+
+bool CGameClientInput::SupportsKeyboard() const
+{
+ const CControllerTree& controllers = GetDefaultControllerTree();
+
+ auto it =
+ std::find_if(controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::KEYBOARD; });
+
+ return it != controllers.GetPorts().end() && !it->GetCompatibleControllers().empty();
+}
+
+bool CGameClientInput::SupportsMouse() const
+{
+ const CControllerTree& controllers = GetDefaultControllerTree();
+
+ auto it =
+ std::find_if(controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::MOUSE; });
+
+ return it != controllers.GetPorts().end() && !it->GetCompatibleControllers().empty();
+}
+
+int CGameClientInput::GetPlayerLimit() const
+{
+ return m_topology->GetPlayerLimit();
+}
+
+bool CGameClientInput::ConnectController(const std::string& portAddress,
+ const ControllerPtr& controller)
+{
+ // Validate parameters
+ if (portAddress.empty() || !controller)
+ return false;
+
+ const CControllerTree& controllerTree = GetDefaultControllerTree();
+
+ // Validate controller
+ const CPortNode& port = controllerTree.GetPort(portAddress);
+ if (!port.IsControllerAccepted(portAddress, controller->ID()))
+ {
+ CLog::Log(LOGERROR, "Failed to open port: Invalid controller \"{}\" on port \"{}\"",
+ controller->ID(), portAddress);
+ return false;
+ }
+
+ const CPortNode& currentPort = GetActiveControllerTree().GetPort(portAddress);
+
+ // Close current ports if any are open
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+ CloseJoysticks(currentPort, inputHandlingLock);
+ inputHandlingLock.reset();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (!m_gameClient.Initialized())
+ return false;
+
+ try
+ {
+ if (!m_struct.toAddon->ConnectController(&m_struct, true, portAddress.c_str(),
+ controller->ID().c_str()))
+ {
+ return false;
+ }
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("ConnectController()");
+ return false;
+ }
+ }
+
+ // Update port state
+ m_portManager->ConnectController(portAddress, true, controller->ID());
+ SetChanged();
+
+ // Update agent input
+ if (controller->Layout().Topology().ProvidesInput())
+ OpenJoystick(portAddress, controller);
+
+ bool bSuccess = true;
+
+ // If port is a multitap, we need to activate its children
+ const CPortNode& updatedPort = GetActiveControllerTree().GetPort(portAddress);
+ const PortVec& childPorts = updatedPort.GetActiveController().GetHub().GetPorts();
+ for (const CPortNode& childPort : childPorts)
+ {
+ const ControllerPtr& childController = childPort.GetActiveController().GetController();
+ if (childController)
+ bSuccess &= ConnectController(childPort.GetAddress(), childController);
+ }
+
+ return bSuccess;
+}
+
+bool CGameClientInput::DisconnectController(const std::string& portAddress)
+{
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+
+ // If port is a multitap, we need to deactivate its children
+ const CPortNode& currentPort = GetActiveControllerTree().GetPort(portAddress);
+ CloseJoysticks(currentPort, inputHandlingLock);
+
+ // If a port was closed, then destroying the lock will block until all
+ // peripheral input handling is complete to avoid invalidating the port's
+ // input handler
+ inputHandlingLock.reset();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (!m_gameClient.Initialized())
+ return false;
+
+ try
+ {
+ if (!m_struct.toAddon->ConnectController(&m_struct, false, portAddress.c_str(), ""))
+ return false;
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("ConnectController()");
+ return false;
+ }
+ }
+
+ // Update port state
+ m_portManager->ConnectController(portAddress, false);
+ SetChanged();
+
+ // Update agent input
+ CloseJoystick(portAddress, inputHandlingLock);
+ inputHandlingLock.reset();
+
+ return true;
+}
+
+void CGameClientInput::SavePorts()
+{
+ // Save port state
+ m_portManager->SaveXMLAsync();
+
+ // Let the observers know that ports have changed
+ NotifyObservers(ObservableMessageGamePortsChanged);
+}
+
+void CGameClientInput::ResetPorts()
+{
+ const CControllerTree& controllerTree = GetDefaultControllerTree();
+ for (const CPortNode& port : controllerTree.GetPorts())
+ ConnectController(port.GetAddress(), port.GetActiveController().GetController());
+}
+
+bool CGameClientInput::HasAgent() const
+{
+ if (!m_joysticks.empty())
+ return true;
+
+ if (m_keyboard)
+ return true;
+
+ if (m_mouse)
+ return true;
+
+ return false;
+}
+
+bool CGameClientInput::OpenKeyboard(const ControllerPtr& controller,
+ const PERIPHERALS::PeripheralPtr& keyboard)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open keyboard, no controller given");
+ return false;
+ }
+
+ if (!keyboard)
+ return false;
+
+ bool bSuccess = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ bSuccess = m_struct.toAddon->EnableKeyboard(&m_struct, true, controller->ID().c_str());
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableKeyboard()");
+ }
+ }
+ }
+
+ if (bSuccess)
+ {
+ m_keyboard =
+ std::make_unique<CGameClientKeyboard>(m_gameClient, controller->ID(), keyboard.get());
+ m_keyboard->SetSource(keyboard);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClientInput::IsKeyboardOpen() const
+{
+ return static_cast<bool>(m_keyboard);
+}
+
+void CGameClientInput::CloseKeyboard()
+{
+ if (m_keyboard)
+ {
+ m_keyboard.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ m_struct.toAddon->EnableKeyboard(&m_struct, false, "");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableKeyboard()");
+ }
+ }
+ }
+}
+
+bool CGameClientInput::OpenMouse(const ControllerPtr& controller,
+ const PERIPHERALS::PeripheralPtr& mouse)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open mouse, no controller given");
+ return false;
+ }
+
+ if (!mouse)
+ return false;
+
+ bool bSuccess = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ bSuccess = m_struct.toAddon->EnableMouse(&m_struct, true, controller->ID().c_str());
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableMouse()");
+ }
+ }
+ }
+
+ if (bSuccess)
+ {
+ m_mouse = std::make_unique<CGameClientMouse>(m_gameClient, controller->ID(), mouse.get());
+ m_mouse->SetSource(mouse);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClientInput::IsMouseOpen() const
+{
+ return static_cast<bool>(m_mouse);
+}
+
+void CGameClientInput::CloseMouse()
+{
+ if (m_mouse)
+ {
+ m_mouse.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ m_struct.toAddon->EnableMouse(&m_struct, false, "");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableMouse()");
+ }
+ }
+ }
+}
+
+bool CGameClientInput::OpenJoystick(const std::string& portAddress, const ControllerPtr& controller)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open port \"{}\", no controller given", portAddress);
+ return false;
+ }
+
+ if (m_joysticks.find(portAddress) != m_joysticks.end())
+ {
+ CLog::Log(LOGERROR, "Failed to open port \"{}\", already open", portAddress);
+ return false;
+ }
+
+ m_joysticks[portAddress].reset(new CGameClientJoystick(m_gameClient, portAddress, controller));
+
+ return true;
+}
+
+void CGameClientInput::CloseJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ std::vector<std::string> portAddresses;
+ for (const auto& it : m_joysticks)
+ portAddresses.emplace_back(it.first);
+
+ for (const std::string& portAddress : portAddresses)
+ CloseJoystick(portAddress, inputHandlingLock);
+}
+
+void CGameClientInput::CloseJoysticks(const CPortNode& port,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ const PortVec& childPorts = port.GetActiveController().GetHub().GetPorts();
+ for (const CPortNode& childPort : childPorts)
+ CloseJoysticks(childPort, inputHandlingLock);
+
+ CloseJoystick(port.GetAddress(), inputHandlingLock);
+}
+
+void CGameClientInput::CloseJoystick(const std::string& portAddress,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ auto it = m_joysticks.find(portAddress);
+ if (it != m_joysticks.end())
+ {
+ if (!inputHandlingLock)
+ {
+ // An input handler is being destroyed. Disable input until the lock is
+ // released. Note: acquiring the lock blocks until all peripheral input
+ // has been handled.
+ inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock();
+ }
+
+ m_joysticks.erase(it);
+ }
+}
+
+void CGameClientInput::HardwareReset()
+{
+ if (m_hardware)
+ m_hardware->OnResetButton();
+}
+
+bool CGameClientInput::ReceiveInputEvent(const game_input_event& event)
+{
+ bool bHandled = false;
+
+ switch (event.type)
+ {
+ case GAME_INPUT_EVENT_MOTOR:
+ if (event.port_address != nullptr && event.feature_name != nullptr)
+ bHandled = SetRumble(event.port_address, event.feature_name, event.motor.magnitude);
+ break;
+ default:
+ break;
+ }
+
+ return bHandled;
+}
+
+bool CGameClientInput::SetRumble(const std::string& portAddress,
+ const std::string& feature,
+ float magnitude)
+{
+ bool bHandled = false;
+
+ auto it = m_joysticks.find(portAddress);
+ if (it != m_joysticks.end())
+ bHandled = it->second->SetRumble(feature, magnitude);
+
+ return bHandled;
+}
+
+ControllerVector CGameClientInput::GetControllers(const CGameClient& gameClient)
+{
+ using namespace ADDON;
+
+ ControllerVector controllers;
+
+ CGameServices& gameServices = CServiceBroker::GetGameServices();
+
+ const auto& dependencies = gameClient.GetDependencies();
+ for (auto it = dependencies.begin(); it != dependencies.end(); ++it)
+ {
+ ControllerPtr controller = gameServices.GetController(it->id);
+ if (controller)
+ controllers.push_back(controller);
+ }
+
+ if (controllers.empty())
+ {
+ // Use the default controller
+ ControllerPtr controller = gameServices.GetDefaultController();
+ if (controller)
+ controllers.push_back(controller);
+ }
+
+ return controllers;
+}
diff --git a/xbmc/games/addons/input/GameClientInput.h b/xbmc/games/addons/input/GameClientInput.h
new file mode 100644
index 0000000..be7e71c
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientInput.h
@@ -0,0 +1,160 @@
+/*
+ * 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 "games/addons/GameClientSubsystem.h"
+#include "games/controllers/ControllerTypes.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "peripherals/PeripheralTypes.h"
+#include "utils/Observer.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+class CCriticalSection;
+struct game_input_event;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+class CGameClientController;
+class CGameClientHardware;
+class CGameClientJoystick;
+class CGameClientKeyboard;
+class CGameClientMouse;
+class CGameClientTopology;
+class CPortManager;
+class IGameInputCallback;
+
+class CGameClientInput : protected CGameClientSubsystem, public Observable
+{
+public:
+ //! @todo de-duplicate
+ using PortAddress = std::string;
+ using JoystickMap = std::map<PortAddress, std::shared_ptr<CGameClientJoystick>>;
+
+ CGameClientInput(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess);
+ ~CGameClientInput() override;
+
+ void Initialize();
+ void Deinitialize();
+
+ void Start(IGameInputCallback* input);
+ void Stop();
+
+ // Input functions
+ bool HasFeature(const std::string& controllerId, const std::string& featureName) const;
+ bool AcceptsInput() const;
+ bool InputEvent(const game_input_event& event);
+
+ // Topology functions
+ const CControllerTree& GetDefaultControllerTree() const;
+ const CControllerTree& GetActiveControllerTree() const;
+ bool SupportsKeyboard() const;
+ bool SupportsMouse() const;
+ int GetPlayerLimit() const;
+ bool ConnectController(const std::string& portAddress, const ControllerPtr& controller);
+ bool DisconnectController(const std::string& portAddress);
+ void SavePorts();
+ void ResetPorts();
+
+ // Joystick functions
+ const JoystickMap& GetJoystickMap() const { return m_joysticks; }
+ void CloseJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+
+ // Keyboard functions
+ bool OpenKeyboard(const ControllerPtr& controller, const PERIPHERALS::PeripheralPtr& keyboard);
+ bool IsKeyboardOpen() const;
+ void CloseKeyboard();
+
+ // Mouse functions
+ bool OpenMouse(const ControllerPtr& controller, const PERIPHERALS::PeripheralPtr& mouse);
+ bool IsMouseOpen() const;
+ void CloseMouse();
+
+ // Agent functions
+ bool HasAgent() const;
+
+ // Hardware input functions
+ void HardwareReset();
+
+ // Input callbacks
+ bool ReceiveInputEvent(const game_input_event& eventStruct);
+
+private:
+ // Private input helpers
+ void LoadTopology();
+ void SetControllerLayouts(const ControllerVector& controllers);
+ bool OpenJoystick(const std::string& portAddress, const ControllerPtr& controller);
+ void CloseJoysticks(const CPortNode& port, PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+ void CloseJoystick(const std::string& portAddress,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+
+ // Private callback helpers
+ bool SetRumble(const std::string& portAddress, const std::string& feature, float magnitude);
+
+ // Helper functions
+ static ControllerVector GetControllers(const CGameClient& gameClient);
+ static void ActivateControllers(CControllerHub& hub);
+
+ // Input properties
+ IGameInputCallback* m_inputCallback = nullptr;
+ std::unique_ptr<CGameClientTopology> m_topology;
+ using ControllerLayoutMap = std::map<std::string, std::unique_ptr<CGameClientController>>;
+ ControllerLayoutMap m_controllerLayouts;
+
+ /*!
+ * \brief Map of port address to joystick handler
+ *
+ * The port address is a string that identifies the adress of the port.
+ *
+ * The joystick handler connects to joystick input of the game client.
+ *
+ * This property is always populated with the default joystick configuration
+ * (i.e. all ports are connected to the first controller they accept).
+ */
+ JoystickMap m_joysticks;
+
+ // TODO: Guard with a mutex
+ std::unique_ptr<CPortManager> m_portManager;
+
+ /*!
+ * \brief Keyboard handler
+ *
+ * This connects to the keyboard input of the game client.
+ */
+ std::unique_ptr<CGameClientKeyboard> m_keyboard;
+
+ /*!
+ * \brief Mouse handler
+ *
+ * This connects to the mouse input of the game client.
+ */
+ std::unique_ptr<CGameClientMouse> m_mouse;
+
+ /*!
+ * \brief Hardware input handler
+ *
+ * This connects to input from game console hardware belonging to the game
+ * client.
+ */
+ std::unique_ptr<CGameClientHardware> m_hardware;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientJoystick.cpp b/xbmc/games/addons/input/GameClientJoystick.cpp
new file mode 100644
index 0000000..d4f083b
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientJoystick.cpp
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2015-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 "GameClientJoystick.h"
+
+#include "GameClientInput.h"
+#include "GameClientTopology.h"
+#include "games/addons/GameClient.h"
+#include "games/controllers/Controller.h"
+#include "games/ports/input/PortInput.h"
+#include "input/joysticks/interfaces/IInputReceiver.h"
+#include "peripherals/devices/Peripheral.h"
+#include "utils/log.h"
+
+#include <assert.h>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientJoystick::CGameClientJoystick(CGameClient& gameClient,
+ const std::string& portAddress,
+ const ControllerPtr& controller)
+ : m_gameClient(gameClient),
+ m_portAddress(portAddress),
+ m_controller(controller),
+ m_portInput(new CPortInput(this))
+{
+ assert(m_controller.get() != NULL);
+}
+
+CGameClientJoystick::~CGameClientJoystick() = default;
+
+void CGameClientJoystick::RegisterInput(JOYSTICK::IInputProvider* inputProvider)
+{
+ m_portInput->RegisterInput(inputProvider);
+}
+
+void CGameClientJoystick::UnregisterInput(JOYSTICK::IInputProvider* inputProvider)
+{
+ m_portInput->UnregisterInput(inputProvider);
+}
+
+std::string CGameClientJoystick::ControllerID(void) const
+{
+ return m_controller->ID();
+}
+
+bool CGameClientJoystick::HasFeature(const std::string& feature) const
+{
+ return m_gameClient.Input().HasFeature(m_controller->ID(), feature);
+}
+
+bool CGameClientJoystick::AcceptsInput(const std::string& feature) const
+{
+ return m_gameClient.Input().AcceptsInput();
+}
+
+bool CGameClientJoystick::OnButtonPress(const std::string& feature, bool bPressed)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_DIGITAL_BUTTON;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.digital_button.pressed = bPressed;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_ANALOG_BUTTON;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.analog_button.magnitude = magnitude;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_ANALOG_STICK;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.analog_stick.x = x;
+ event.analog_stick.y = y;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnAccelerometerMotion(const std::string& feature,
+ float x,
+ float y,
+ float z)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_ACCELEROMETER;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.accelerometer.x = x;
+ event.accelerometer.y = y;
+ event.accelerometer.z = z;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_AXIS;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.axis.position = position;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_AXIS;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.axis.position = position;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+std::string CGameClientJoystick::GetControllerAddress() const
+{
+ return CGameClientTopology::MakeAddress(m_portAddress, m_controller->ID());
+}
+
+std::string CGameClientJoystick::GetSourceLocation() const
+{
+ if (m_sourcePeripheral)
+ return m_sourcePeripheral->Location();
+
+ return "";
+}
+
+void CGameClientJoystick::SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral)
+{
+ m_sourcePeripheral = std::move(sourcePeripheral);
+}
+
+void CGameClientJoystick::ClearSource()
+{
+ m_sourcePeripheral.reset();
+}
+
+bool CGameClientJoystick::SetRumble(const std::string& feature, float magnitude)
+{
+ bool bHandled = false;
+
+ if (InputReceiver())
+ bHandled = InputReceiver()->SetRumbleState(feature, magnitude);
+
+ return bHandled;
+}
diff --git a/xbmc/games/addons/input/GameClientJoystick.h b/xbmc/games/addons/input/GameClientJoystick.h
new file mode 100644
index 0000000..cfb1709
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientJoystick.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015-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 "games/controllers/ControllerTypes.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "peripherals/PeripheralTypes.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+class CPortInput;
+
+/*!
+ * \ingroup games
+ * \brief Handles game controller events for games.
+ *
+ * Listens to game controller events and forwards them to the games (as game_input_event).
+ */
+class CGameClientJoystick : public JOYSTICK::IInputHandler
+{
+public:
+ /*!
+ * \brief Constructor.
+ * \param addon The game client implementation.
+ * \param port The port this game controller is associated with.
+ * \param controller The game controller which is used (for controller mapping).
+ * \param dllStruct The emulator or game to which the events are sent.
+ */
+ CGameClientJoystick(CGameClient& addon,
+ const std::string& portAddress,
+ const ControllerPtr& controller);
+
+ ~CGameClientJoystick() override;
+
+ void RegisterInput(JOYSTICK::IInputProvider* inputProvider);
+ void UnregisterInput(JOYSTICK::IInputProvider* inputProvider);
+
+ // Implementation of IInputHandler
+ std::string ControllerID() const override;
+ bool HasFeature(const std::string& feature) const override;
+ 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 {}
+
+ // Input accessors
+ const std::string& GetPortAddress() const { return m_portAddress; }
+ const ControllerPtr& GetController() const { return m_controller; }
+ std::string GetControllerAddress() const;
+ const PERIPHERALS::PeripheralPtr& GetSource() const { return m_sourcePeripheral; }
+ std::string GetSourceLocation() const;
+
+ // Input mutators
+ void SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral);
+ void ClearSource();
+
+ // Input handlers
+ bool SetRumble(const std::string& feature, float magnitude);
+
+private:
+ // Construction parameters
+ CGameClient& m_gameClient;
+ const std::string m_portAddress;
+ const ControllerPtr m_controller;
+
+ // Input parameters
+ std::unique_ptr<CPortInput> m_portInput;
+ PERIPHERALS::PeripheralPtr m_sourcePeripheral;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientKeyboard.cpp b/xbmc/games/addons/input/GameClientKeyboard.cpp
new file mode 100644
index 0000000..e6f48c9
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientKeyboard.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015-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 "GameClientKeyboard.h"
+
+#include "GameClientInput.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/GameClientTranslator.h"
+#include "input/keyboard/interfaces/IKeyboardInputProvider.h"
+#include "utils/log.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+#define BUTTON_INDEX_MASK 0x01ff
+
+CGameClientKeyboard::CGameClientKeyboard(CGameClient& gameClient,
+ std::string controllerId,
+ KEYBOARD::IKeyboardInputProvider* inputProvider)
+ : m_gameClient(gameClient),
+ m_controllerId(std::move(controllerId)),
+ m_inputProvider(inputProvider)
+{
+ m_inputProvider->RegisterKeyboardHandler(this, false);
+}
+
+CGameClientKeyboard::~CGameClientKeyboard()
+{
+ m_inputProvider->UnregisterKeyboardHandler(this);
+}
+
+std::string CGameClientKeyboard::ControllerID() const
+{
+ return m_controllerId;
+}
+
+bool CGameClientKeyboard::HasKey(const KEYBOARD::KeyName& key) const
+{
+ return m_gameClient.Input().HasFeature(ControllerID(), key);
+}
+
+bool CGameClientKeyboard::OnKeyPress(const KEYBOARD::KeyName& key,
+ KEYBOARD::Modifier mod,
+ uint32_t unicode)
+{
+ // Only allow activated input in fullscreen game
+ if (!m_gameClient.Input().AcceptsInput())
+ {
+ CLog::Log(LOGDEBUG, "GAME: key press ignored, not in fullscreen game");
+ return false;
+ }
+
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_KEY;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_KEYBOARD;
+ event.port_address = ""; // Not used
+ event.feature_name = key.c_str();
+ event.key.pressed = true;
+ event.key.unicode = unicode;
+ event.key.modifiers = CGameClientTranslator::GetModifiers(mod);
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientKeyboard::OnKeyRelease(const KEYBOARD::KeyName& key,
+ KEYBOARD::Modifier mod,
+ uint32_t unicode)
+{
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_KEY;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_KEYBOARD;
+ event.port_address = ""; // Not used
+ event.feature_name = key.c_str();
+ event.key.pressed = false;
+ event.key.unicode = unicode;
+ event.key.modifiers = CGameClientTranslator::GetModifiers(mod);
+
+ m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientKeyboard::SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral)
+{
+ m_sourcePeripheral = std::move(sourcePeripheral);
+}
+
+void CGameClientKeyboard::ClearSource()
+{
+ m_sourcePeripheral.reset();
+}
diff --git a/xbmc/games/addons/input/GameClientKeyboard.h b/xbmc/games/addons/input/GameClientKeyboard.h
new file mode 100644
index 0000000..a6103f2
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientKeyboard.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015-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/keyboard/interfaces/IKeyboardInputHandler.h"
+#include "peripherals/PeripheralTypes.h"
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+class IKeyboardInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \ingroup games
+ * \brief Handles keyboard events for games.
+ *
+ * Listens to keyboard events and forwards them to the games (as game_input_event).
+ */
+class CGameClientKeyboard : public KEYBOARD::IKeyboardInputHandler
+{
+public:
+ /*!
+ * \brief Constructor registers for keyboard events at CInputManager.
+ * \param gameClient The game client implementation.
+ * \param controllerId The controller profile used for input
+ * \param dllStruct The emulator or game to which the events are sent.
+ * \param inputProvider The interface providing us with keyboard input.
+ */
+ CGameClientKeyboard(CGameClient& gameClient,
+ std::string controllerId,
+ KEYBOARD::IKeyboardInputProvider* inputProvider);
+
+ /*!
+ * \brief Destructor unregisters from keyboard events from CInputManager.
+ */
+ ~CGameClientKeyboard() override;
+
+ // implementation of IKeyboardInputHandler
+ std::string ControllerID() const override;
+ bool HasKey(const KEYBOARD::KeyName& key) const override;
+ bool OnKeyPress(const KEYBOARD::KeyName& key, KEYBOARD::Modifier mod, uint32_t unicode) override;
+ void OnKeyRelease(const KEYBOARD::KeyName& key,
+ KEYBOARD::Modifier mod,
+ uint32_t unicode) override;
+
+ // Input accessors
+ const std::string& GetControllerID() const { return m_controllerId; }
+ const PERIPHERALS::PeripheralPtr& GetSource() const { return m_sourcePeripheral; }
+
+ // Input mutators
+ void SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral);
+ void ClearSource();
+
+private:
+ // Construction parameters
+ CGameClient& m_gameClient;
+ const std::string m_controllerId;
+ KEYBOARD::IKeyboardInputProvider* const m_inputProvider;
+
+ // Input parameters
+ PERIPHERALS::PeripheralPtr m_sourcePeripheral;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientMouse.cpp b/xbmc/games/addons/input/GameClientMouse.cpp
new file mode 100644
index 0000000..5cc9676
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientMouse.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015-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 "GameClientMouse.h"
+
+#include "GameClientInput.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/GameClient.h"
+#include "input/mouse/interfaces/IMouseInputProvider.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientMouse::CGameClientMouse(CGameClient& gameClient,
+ std::string controllerId,
+ MOUSE::IMouseInputProvider* inputProvider)
+ : m_gameClient(gameClient),
+ m_controllerId(std::move(controllerId)),
+ m_inputProvider(inputProvider)
+{
+ inputProvider->RegisterMouseHandler(this, false);
+}
+
+CGameClientMouse::~CGameClientMouse()
+{
+ m_inputProvider->UnregisterMouseHandler(this);
+}
+
+std::string CGameClientMouse::ControllerID(void) const
+{
+ return m_controllerId;
+}
+
+bool CGameClientMouse::OnMotion(const std::string& relpointer, int dx, int dy)
+{
+ // Only allow activated input in fullscreen game
+ if (!m_gameClient.Input().AcceptsInput())
+ {
+ return false;
+ }
+
+ const std::string controllerId = ControllerID();
+
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_RELATIVE_POINTER;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_MOUSE;
+ event.port_address = ""; // Not used
+ event.feature_name = relpointer.c_str();
+ event.rel_pointer.x = dx;
+ event.rel_pointer.y = dy;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientMouse::OnButtonPress(const std::string& button)
+{
+ // Only allow activated input in fullscreen game
+ if (!m_gameClient.Input().AcceptsInput())
+ {
+ return false;
+ }
+
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_DIGITAL_BUTTON;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_MOUSE;
+ event.port_address = ""; // Not used
+ event.feature_name = button.c_str();
+ event.digital_button.pressed = true;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientMouse::OnButtonRelease(const std::string& button)
+{
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_DIGITAL_BUTTON;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_MOUSE;
+ event.port_address = ""; // Not used
+ event.feature_name = button.c_str();
+ event.digital_button.pressed = false;
+
+ m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientMouse::SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral)
+{
+ m_sourcePeripheral = std::move(sourcePeripheral);
+}
+
+void CGameClientMouse::ClearSource()
+{
+ m_sourcePeripheral.reset();
+}
diff --git a/xbmc/games/addons/input/GameClientMouse.h b/xbmc/games/addons/input/GameClientMouse.h
new file mode 100644
index 0000000..4da592f
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientMouse.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015-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/mouse/interfaces/IMouseInputHandler.h"
+#include "peripherals/PeripheralTypes.h"
+
+namespace KODI
+{
+namespace MOUSE
+{
+class IMouseInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \ingroup games
+ * \brief Handles mouse events for games.
+ *
+ * Listens to mouse events and forwards them to the games (as game_input_event).
+ */
+class CGameClientMouse : public MOUSE::IMouseInputHandler
+{
+public:
+ /*!
+ * \brief Constructor registers for mouse events at CInputManager.
+ * \param gameClient The game client implementation.
+ * \param controllerId The controller profile used for input
+ * \param dllStruct The emulator or game to which the events are sent.
+ * \param inputProvider The interface providing us with mouse input.
+ */
+ CGameClientMouse(CGameClient& gameClient,
+ std::string controllerId,
+ MOUSE::IMouseInputProvider* inputProvider);
+
+ /*!
+ * \brief Destructor unregisters from mouse events from CInputManager.
+ */
+ ~CGameClientMouse() override;
+
+ // implementation of IMouseInputHandler
+ std::string ControllerID() const override;
+ bool OnMotion(const std::string& relpointer, int dx, int dy) override;
+ bool OnButtonPress(const std::string& button) override;
+ void OnButtonRelease(const std::string& button) override;
+
+ // Input accessors
+ const std::string& GetControllerID() const { return m_controllerId; }
+ const PERIPHERALS::PeripheralPtr& GetSource() const { return m_sourcePeripheral; }
+
+ // Input mutators
+ void SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral);
+ void ClearSource();
+
+private:
+ // Construction parameters
+ CGameClient& m_gameClient;
+ const std::string m_controllerId;
+ MOUSE::IMouseInputProvider* const m_inputProvider;
+
+ // Input parameters
+ PERIPHERALS::PeripheralPtr m_sourcePeripheral;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientPort.cpp b/xbmc/games/addons/input/GameClientPort.cpp
new file mode 100644
index 0000000..c582fff
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientPort.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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 "GameClientPort.h"
+
+#include "GameClientDevice.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/GameClientTranslator.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientPort::CGameClientPort(const game_input_port& port)
+ : m_type(CGameClientTranslator::TranslatePortType(port.type)),
+ m_portId(port.port_id ? port.port_id : ""),
+ m_forceConnected(port.force_connected)
+{
+ if (port.accepted_devices != nullptr)
+ {
+ for (unsigned int i = 0; i < port.device_count; i++)
+ {
+ std::unique_ptr<CGameClientDevice> device(new CGameClientDevice(port.accepted_devices[i]));
+
+ if (device->Controller() != CController::EmptyPtr)
+ m_acceptedDevices.emplace_back(std::move(device));
+ }
+ }
+}
+
+CGameClientPort::CGameClientPort(const ControllerVector& controllers)
+ : m_type(PORT_TYPE::CONTROLLER), m_portId(DEFAULT_PORT_ID)
+{
+ for (const auto& controller : controllers)
+ m_acceptedDevices.emplace_back(new CGameClientDevice(controller));
+}
+
+CGameClientPort::CGameClientPort(const game_input_port& logicalPort,
+ const CPhysicalPort& physicalPort)
+ : m_type(PORT_TYPE::CONTROLLER),
+ m_portId(physicalPort.ID()),
+ m_forceConnected(logicalPort.force_connected)
+{
+ if (logicalPort.accepted_devices != nullptr)
+ {
+ for (unsigned int i = 0; i < logicalPort.device_count; i++)
+ {
+ // Ensure device is physically compatible
+ const game_input_device& deviceStruct = logicalPort.accepted_devices[i];
+ std::string controllerId = deviceStruct.controller_id ? deviceStruct.controller_id : "";
+
+ if (physicalPort.IsCompatible(controllerId))
+ {
+ std::unique_ptr<CGameClientDevice> device(new CGameClientDevice(deviceStruct));
+
+ if (device->Controller() != CController::EmptyPtr)
+ m_acceptedDevices.emplace_back(std::move(device));
+ }
+ }
+ }
+}
+
+CGameClientPort::~CGameClientPort() = default;
diff --git a/xbmc/games/addons/input/GameClientPort.h b/xbmc/games/addons/input/GameClientPort.h
new file mode 100644
index 0000000..80fccef
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientPort.h
@@ -0,0 +1,103 @@
+/*
+ * 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 "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+
+struct game_input_device;
+struct game_input_port;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalPort;
+
+/*!
+ * \ingroup games
+ * \brief Represents a port that devices can connect to
+ */
+class CGameClientPort
+{
+public:
+ /*!
+ * \brief Construct a hardware port
+ *
+ * \param port The hardware port Game API struct
+ */
+ CGameClientPort(const game_input_port& port);
+
+ /*!
+ * \brief Construct a hardware port that accepts the given controllers
+ *
+ * \param controllers List of accepted controller profiles
+ *
+ * The port is given the ID specified by DEFAULT_PORT_ID.
+ */
+ CGameClientPort(const ControllerVector& controllers);
+
+ /*!
+ * \brief Construct a controller port
+ *
+ * \param logicalPort The logical port Game API struct
+ * \param physicalPort The physical port definition
+ *
+ * The physical port is defined by the controller profile. This definition
+ * specifies which controllers the port is physically compatible with.
+ *
+ * The logical port is defined by the emulator's input topology. This
+ * definition specifies which controllers the emulator's logic can handle.
+ *
+ * Obviously, the controllers specified by the logical port must be a subset
+ * of the controllers supported by the physical port.
+ */
+ CGameClientPort(const game_input_port& logicalPort, const CPhysicalPort& physicalPort);
+
+ /*!
+ * \brief Destructor
+ */
+ ~CGameClientPort();
+
+ /*!
+ * \brief Get the port type
+ *
+ * The port type identifies if this port is for a keyboard, mouse, or
+ * controller.
+ */
+ PORT_TYPE PortType() const { return m_type; }
+
+ /*!
+ * \brief Get the ID of the port
+ *
+ * The ID is used when creating a toplogical address for the port.
+ */
+ const std::string& ID() const { return m_portId; }
+
+ /*!
+ * \brief True if a controller must be connected, preventing the disconnected
+ * option from being shown to the user
+ */
+ bool ForceConnected() const { return m_forceConnected; }
+
+ /*!
+ * \brief Get the list of devices accepted by this port
+ */
+ const GameClientDeviceVec& Devices() const { return m_acceptedDevices; }
+
+private:
+ PORT_TYPE m_type;
+ std::string m_portId;
+ bool m_forceConnected{false};
+ GameClientDeviceVec m_acceptedDevices;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientTopology.cpp b/xbmc/games/addons/input/GameClientTopology.cpp
new file mode 100644
index 0000000..e1e9757
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientTopology.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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 "GameClientTopology.h"
+
+#include "GameClientDevice.h"
+#include "GameClientPort.h"
+#include "games/controllers/Controller.h"
+
+#include <sstream>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+#define CONTROLLER_ADDRESS_SEPARATOR "/"
+
+CGameClientTopology::CGameClientTopology(GameClientPortVec ports, int playerLimit)
+ : m_ports(std::move(ports)), m_playerLimit(playerLimit), m_controllers(GetControllerTree(m_ports))
+{
+}
+
+void CGameClientTopology::Clear()
+{
+ m_ports.clear();
+ m_controllers.Clear();
+}
+
+CControllerTree CGameClientTopology::GetControllerTree(const GameClientPortVec& ports)
+{
+ CControllerTree tree;
+
+ PortVec controllerPorts;
+ for (const GameClientPortPtr& port : ports)
+ {
+ CPortNode portNode = GetPortNode(port, "");
+ controllerPorts.emplace_back(std::move(portNode));
+ }
+
+ tree.SetPorts(std::move(controllerPorts));
+
+ return tree;
+}
+
+CPortNode CGameClientTopology::GetPortNode(const GameClientPortPtr& port,
+ const std::string& controllerAddress)
+{
+ CPortNode portNode;
+
+ std::string portAddress = MakeAddress(controllerAddress, port->ID());
+
+ portNode.SetConnected(false);
+ portNode.SetPortType(port->PortType());
+ portNode.SetPortID(port->ID());
+ portNode.SetAddress(portAddress);
+ portNode.SetForceConnected(port->ForceConnected());
+
+ ControllerNodeVec nodes;
+ for (const GameClientDevicePtr& device : port->Devices())
+ {
+ CControllerNode controllerNode = GetControllerNode(device, portAddress);
+ nodes.emplace_back(std::move(controllerNode));
+ }
+ portNode.SetCompatibleControllers(std::move(nodes));
+
+ return portNode;
+}
+
+CControllerNode CGameClientTopology::GetControllerNode(const GameClientDevicePtr& device,
+ const std::string& portAddress)
+{
+ CControllerNode controllerNode;
+
+ const std::string controllerAddress = MakeAddress(portAddress, device->Controller()->ID());
+
+ controllerNode.SetController(device->Controller());
+ controllerNode.SetPortAddress(portAddress);
+ controllerNode.SetControllerAddress(controllerAddress);
+
+ PortVec ports;
+ for (const GameClientPortPtr& port : device->Ports())
+ {
+ CPortNode portNode = GetPortNode(port, controllerAddress);
+ ports.emplace_back(std::move(portNode));
+ }
+
+ CControllerHub controllerHub;
+ controllerHub.SetPorts(std::move(ports));
+ controllerNode.SetHub(std::move(controllerHub));
+
+ return controllerNode;
+}
+
+std::string CGameClientTopology::MakeAddress(const std::string& baseAddress,
+ const std::string& nodeId)
+{
+ std::ostringstream address;
+
+ if (!baseAddress.empty())
+ address << baseAddress;
+
+ address << CONTROLLER_ADDRESS_SEPARATOR << nodeId;
+
+ return address.str();
+}
diff --git a/xbmc/games/addons/input/GameClientTopology.h b/xbmc/games/addons/input/GameClientTopology.h
new file mode 100644
index 0000000..10b40f1
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientTopology.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "games/GameTypes.h"
+#include "games/controllers/types/ControllerTree.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClientTopology
+{
+public:
+ CGameClientTopology() = default;
+ CGameClientTopology(GameClientPortVec ports, int playerLimit);
+
+ void Clear();
+
+ int GetPlayerLimit() const { return m_playerLimit; }
+
+ const CControllerTree& GetControllerTree() const { return m_controllers; }
+ CControllerTree& GetControllerTree() { return m_controllers; }
+
+ // Utility function
+ static std::string MakeAddress(const std::string& baseAddress, const std::string& nodeId);
+
+private:
+ static CControllerTree GetControllerTree(const GameClientPortVec& ports);
+ static CPortNode GetPortNode(const GameClientPortPtr& port, const std::string& controllerAddress);
+ static CControllerNode GetControllerNode(const GameClientDevicePtr& device,
+ const std::string& portAddress);
+
+ // Game API parameters
+ GameClientPortVec m_ports;
+ int m_playerLimit = -1;
+
+ // Controller parameters
+ CControllerTree m_controllers;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/CMakeLists.txt b/xbmc/games/addons/streams/CMakeLists.txt
new file mode 100644
index 0000000..f8de0e6
--- /dev/null
+++ b/xbmc/games/addons/streams/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES GameClientStreamAudio.cpp
+ GameClientStreams.cpp
+ GameClientStreamSwFramebuffer.cpp
+ GameClientStreamVideo.cpp
+)
+
+set(HEADERS GameClientStreamAudio.h
+ GameClientStreams.h
+ GameClientStreamSwFramebuffer.h
+ GameClientStreamVideo.h
+ IGameClientStream.h
+)
+
+core_add_library(game_addon_streams)
diff --git a/xbmc/games/addons/streams/GameClientStreamAudio.cpp b/xbmc/games/addons/streams/GameClientStreamAudio.cpp
new file mode 100644
index 0000000..a38f054
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamAudio.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 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 "GameClientStreamAudio.h"
+
+#include "cores/RetroPlayer/streams/RetroPlayerAudio.h"
+#include "games/addons/GameClientTranslator.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientStreamAudio::CGameClientStreamAudio(double sampleRate) : m_sampleRate(sampleRate)
+{
+}
+
+bool CGameClientStreamAudio::OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties)
+{
+ RETRO::CRetroPlayerAudio* audioStream = dynamic_cast<RETRO::CRetroPlayerAudio*>(stream);
+ if (audioStream == nullptr)
+ {
+ CLog::Log(LOGERROR, "GAME: RetroPlayer stream is not an audio stream");
+ return false;
+ }
+
+ std::unique_ptr<RETRO::AudioStreamProperties> audioProperties(
+ TranslateProperties(properties.audio, m_sampleRate));
+ if (audioProperties)
+ {
+ if (audioStream->OpenStream(static_cast<const RETRO::StreamProperties&>(*audioProperties)))
+ m_stream = stream;
+ }
+
+ return m_stream != nullptr;
+}
+
+void CGameClientStreamAudio::CloseStream()
+{
+ if (m_stream != nullptr)
+ {
+ m_stream->CloseStream();
+ m_stream = nullptr;
+ }
+}
+
+void CGameClientStreamAudio::AddData(const game_stream_packet& packet)
+{
+ if (packet.type != GAME_STREAM_AUDIO)
+ return;
+
+ if (m_stream != nullptr)
+ {
+ const game_stream_audio_packet& audio = packet.audio;
+
+ RETRO::AudioStreamPacket audioPacket{audio.data, audio.size};
+
+ m_stream->AddStreamData(static_cast<RETRO::StreamPacket&>(audioPacket));
+ }
+}
+
+RETRO::AudioStreamProperties* CGameClientStreamAudio::TranslateProperties(
+ const game_stream_audio_properties& properties, double sampleRate)
+{
+ const RETRO::PCMFormat pcmFormat = CGameClientTranslator::TranslatePCMFormat(properties.format);
+ if (pcmFormat == RETRO::PCMFormat::FMT_UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "GAME: Unknown PCM format: {}", static_cast<int>(properties.format));
+ return nullptr;
+ }
+
+ RETRO::AudioChannelMap channelMap = {{RETRO::AudioChannel::CH_NULL}};
+ unsigned int i = 0;
+ if (properties.channel_map != nullptr)
+ {
+ for (const GAME_AUDIO_CHANNEL* channelPtr = properties.channel_map; *channelPtr != GAME_CH_NULL;
+ channelPtr++)
+ {
+ RETRO::AudioChannel channel = CGameClientTranslator::TranslateAudioChannel(*channelPtr);
+ if (channel == RETRO::AudioChannel::CH_NULL)
+ {
+ CLog::Log(LOGERROR, "GAME: Unknown channel ID: {}", static_cast<int>(*channelPtr));
+ return nullptr;
+ }
+
+ channelMap[i++] = channel;
+ if (i + 1 >= channelMap.size())
+ break;
+ }
+ }
+ channelMap[i] = RETRO::AudioChannel::CH_NULL;
+
+ if (channelMap[0] == RETRO::AudioChannel::CH_NULL)
+ {
+ CLog::Log(LOGERROR, "GAME: Empty channel layout");
+ return nullptr;
+ }
+
+ return new RETRO::AudioStreamProperties{pcmFormat, sampleRate, channelMap};
+}
diff --git a/xbmc/games/addons/streams/GameClientStreamAudio.h b/xbmc/games/addons/streams/GameClientStreamAudio.h
new file mode 100644
index 0000000..d48b0a6
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamAudio.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 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 "IGameClientStream.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+
+#include <vector>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRetroPlayerStream;
+struct AudioStreamProperties;
+} // namespace RETRO
+
+namespace GAME
+{
+
+class CGameClientStreamAudio : public IGameClientStream
+{
+public:
+ CGameClientStreamAudio(double sampleRate);
+ ~CGameClientStreamAudio() override { CloseStream(); }
+
+ // Implementation of IGameClientStream
+ bool OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties) override;
+ void CloseStream() override;
+ void AddData(const game_stream_packet& packet) override;
+
+private:
+ // Utility functions
+ static RETRO::AudioStreamProperties* TranslateProperties(
+ const game_stream_audio_properties& properties, double sampleRate);
+
+ // Construction parameters
+ const double m_sampleRate;
+
+ // Stream parameters
+ RETRO::IRetroPlayerStream* m_stream = nullptr;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp
new file mode 100644
index 0000000..4d1e8af
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 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 "GameClientStreamSwFramebuffer.h"
+
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "cores/RetroPlayer/streams/RetroPlayerVideo.h"
+#include "games/addons/GameClientTranslator.h"
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGameClientStreamSwFramebuffer::GetBuffer(unsigned int width,
+ unsigned int height,
+ game_stream_buffer& buffer)
+{
+ if (m_stream != nullptr)
+ {
+ RETRO::VideoStreamBuffer streamBuffer;
+ if (m_stream->GetStreamBuffer(width, height, static_cast<RETRO::StreamBuffer&>(streamBuffer)))
+ {
+ buffer.type = GAME_STREAM_SW_FRAMEBUFFER;
+
+ game_stream_sw_framebuffer_buffer& framebuffer = buffer.sw_framebuffer;
+
+ framebuffer.format = CGameClientTranslator::TranslatePixelFormat(streamBuffer.pixfmt);
+ framebuffer.data = streamBuffer.data;
+ framebuffer.size = streamBuffer.size;
+
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h
new file mode 100644
index 0000000..2b17dac
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 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 "GameClientStreamVideo.h"
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameClientStreamSwFramebuffer : public CGameClientStreamVideo
+{
+public:
+ CGameClientStreamSwFramebuffer() = default;
+ ~CGameClientStreamSwFramebuffer() override = default;
+
+ // Implementation of IGameClientStream via CGameClientStreamVideo
+ bool GetBuffer(unsigned int width, unsigned int height, game_stream_buffer& buffer) override;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/GameClientStreamVideo.cpp b/xbmc/games/addons/streams/GameClientStreamVideo.cpp
new file mode 100644
index 0000000..d110b28
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamVideo.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 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 "GameClientStreamVideo.h"
+
+#include "cores/RetroPlayer/streams/RetroPlayerVideo.h"
+#include "games/addons/GameClientTranslator.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGameClientStreamVideo::OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties)
+{
+ RETRO::CRetroPlayerVideo* videoStream = dynamic_cast<RETRO::CRetroPlayerVideo*>(stream);
+ if (videoStream == nullptr)
+ {
+ CLog::Log(LOGERROR, "GAME: RetroPlayer stream is not a video stream");
+ return false;
+ }
+
+ std::unique_ptr<RETRO::VideoStreamProperties> videoProperties(
+ TranslateProperties(properties.video));
+ if (videoProperties)
+ {
+ if (videoStream->OpenStream(static_cast<const RETRO::StreamProperties&>(*videoProperties)))
+ m_stream = stream;
+ }
+
+ return m_stream != nullptr;
+}
+
+void CGameClientStreamVideo::CloseStream()
+{
+ if (m_stream != nullptr)
+ {
+ m_stream->CloseStream();
+ m_stream = nullptr;
+ }
+}
+
+void CGameClientStreamVideo::AddData(const game_stream_packet& packet)
+{
+ if (packet.type != GAME_STREAM_VIDEO && packet.type != GAME_STREAM_SW_FRAMEBUFFER)
+ return;
+
+ if (m_stream != nullptr)
+ {
+ const game_stream_video_packet& video = packet.video;
+
+ RETRO::VideoRotation rotation = CGameClientTranslator::TranslateRotation(video.rotation);
+
+ RETRO::VideoStreamPacket videoPacket{
+ video.width, video.height, rotation, video.data, video.size,
+ };
+
+ m_stream->AddStreamData(static_cast<const RETRO::StreamPacket&>(videoPacket));
+ }
+}
+
+RETRO::VideoStreamProperties* CGameClientStreamVideo::TranslateProperties(
+ const game_stream_video_properties& properties)
+{
+ const AVPixelFormat pixelFormat = CGameClientTranslator::TranslatePixelFormat(properties.format);
+ if (pixelFormat == AV_PIX_FMT_NONE)
+ {
+ CLog::Log(LOGERROR, "GAME: Unknown pixel format: {}", properties.format);
+ return nullptr;
+ }
+
+ const unsigned int nominalWidth = properties.nominal_width;
+ const unsigned int nominalHeight = properties.nominal_height;
+ if (nominalWidth == 0 || nominalHeight == 0)
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid nominal dimensions: {}x{}", nominalWidth, nominalHeight);
+ return nullptr;
+ }
+
+ const unsigned int maxWidth = properties.max_width;
+ const unsigned int maxHeight = properties.max_height;
+ if (maxWidth == 0 || maxHeight == 0)
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid max dimensions: {}x{}", maxWidth, maxHeight);
+ return nullptr;
+ }
+
+ if (nominalWidth > maxWidth || nominalHeight > maxHeight)
+ CLog::Log(LOGERROR, "GAME: Nominal dimensions ({}x{}) bigger than max dimensions ({}x{})",
+ nominalWidth, nominalHeight, maxWidth, maxHeight);
+
+ float pixelAspectRatio;
+
+ // Game API: If aspect_ratio is <= 0.0, an aspect ratio of
+ // (nominal_width / nominal_height) is assumed
+ if (properties.aspect_ratio <= 0.0f)
+ pixelAspectRatio = 1.0f;
+ else
+ pixelAspectRatio = properties.aspect_ratio * nominalHeight / nominalWidth;
+
+ return new RETRO::VideoStreamProperties{pixelFormat, nominalWidth, nominalHeight,
+ maxWidth, maxHeight, pixelAspectRatio};
+}
diff --git a/xbmc/games/addons/streams/GameClientStreamVideo.h b/xbmc/games/addons/streams/GameClientStreamVideo.h
new file mode 100644
index 0000000..4c90c2c
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamVideo.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 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 "IGameClientStream.h"
+
+struct game_stream_video_properties;
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRetroPlayerStream;
+struct VideoStreamProperties;
+} // namespace RETRO
+
+namespace GAME
+{
+
+class CGameClientStreamVideo : public IGameClientStream
+{
+public:
+ CGameClientStreamVideo() = default;
+ ~CGameClientStreamVideo() override { CloseStream(); }
+
+ // Implementation of IGameClientStream
+ bool OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties) override;
+ void CloseStream() override;
+ void AddData(const game_stream_packet& packet) override;
+
+protected:
+ // Stream parameters
+ RETRO::IRetroPlayerStream* m_stream = nullptr;
+
+private:
+ // Utility functions
+ static RETRO::VideoStreamProperties* TranslateProperties(
+ const game_stream_video_properties& properties);
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/GameClientStreams.cpp b/xbmc/games/addons/streams/GameClientStreams.cpp
new file mode 100644
index 0000000..7d005ea
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreams.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 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 "GameClientStreams.h"
+
+#include "GameClientStreamAudio.h"
+#include "GameClientStreamSwFramebuffer.h"
+#include "GameClientStreamVideo.h"
+#include "cores/RetroPlayer/streams/IRetroPlayerStream.h"
+#include "cores/RetroPlayer/streams/IStreamManager.h"
+#include "cores/RetroPlayer/streams/RetroPlayerStreamTypes.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/GameClientTranslator.h"
+#include "utils/log.h"
+
+#include <memory>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientStreams::CGameClientStreams(CGameClient& gameClient) : m_gameClient(gameClient)
+{
+}
+
+void CGameClientStreams::Initialize(RETRO::IStreamManager& streamManager)
+{
+ m_streamManager = &streamManager;
+}
+
+void CGameClientStreams::Deinitialize()
+{
+ m_streamManager = nullptr;
+}
+
+IGameClientStream* CGameClientStreams::OpenStream(const game_stream_properties& properties)
+{
+ if (m_streamManager == nullptr)
+ return nullptr;
+
+ RETRO::StreamType retroStreamType;
+ if (!CGameClientTranslator::TranslateStreamType(properties.type, retroStreamType))
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid stream type: {}", static_cast<int>(properties.type));
+ return nullptr;
+ }
+
+ std::unique_ptr<IGameClientStream> gameStream = CreateStream(properties.type);
+ if (!gameStream)
+ {
+ CLog::Log(LOGERROR, "GAME: No stream implementation for type: {}",
+ static_cast<int>(properties.type));
+ return nullptr;
+ }
+
+ RETRO::StreamPtr retroStream = m_streamManager->CreateStream(retroStreamType);
+ if (!retroStream)
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid RetroPlayer stream type: {}",
+ static_cast<int>(retroStreamType));
+ return nullptr;
+ }
+
+ if (!gameStream->OpenStream(retroStream.get(), properties))
+ {
+ CLog::Log(LOGERROR, "GAME: Failed to open audio stream");
+ return nullptr;
+ }
+
+ m_streams[gameStream.get()] = std::move(retroStream);
+
+ return gameStream.release();
+}
+
+void CGameClientStreams::CloseStream(IGameClientStream* stream)
+{
+ if (stream != nullptr)
+ {
+ std::unique_ptr<IGameClientStream> streamHolder(stream);
+ streamHolder->CloseStream();
+
+ m_streamManager->CloseStream(std::move(m_streams[stream]));
+ m_streams.erase(stream);
+ }
+}
+
+std::unique_ptr<IGameClientStream> CGameClientStreams::CreateStream(
+ GAME_STREAM_TYPE streamType) const
+{
+ std::unique_ptr<IGameClientStream> gameStream;
+
+ switch (streamType)
+ {
+ case GAME_STREAM_AUDIO:
+ {
+ gameStream.reset(new CGameClientStreamAudio(m_gameClient.GetSampleRate()));
+ break;
+ }
+ case GAME_STREAM_VIDEO:
+ {
+ gameStream.reset(new CGameClientStreamVideo);
+ break;
+ }
+ case GAME_STREAM_SW_FRAMEBUFFER:
+ {
+ gameStream.reset(new CGameClientStreamSwFramebuffer);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return gameStream;
+}
diff --git a/xbmc/games/addons/streams/GameClientStreams.h b/xbmc/games/addons/streams/GameClientStreams.h
new file mode 100644
index 0000000..6f3f639
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreams.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "cores/RetroPlayer/streams/RetroPlayerStreamTypes.h"
+
+#include <map>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IStreamManager;
+}
+
+namespace GAME
+{
+
+class CGameClient;
+class IGameClientStream;
+
+class CGameClientStreams
+{
+public:
+ CGameClientStreams(CGameClient& gameClient);
+
+ void Initialize(RETRO::IStreamManager& streamManager);
+ void Deinitialize();
+
+ IGameClientStream* OpenStream(const game_stream_properties& properties);
+ void CloseStream(IGameClientStream* stream);
+
+private:
+ // Utility functions
+ std::unique_ptr<IGameClientStream> CreateStream(GAME_STREAM_TYPE streamType) const;
+
+ // Construction parameters
+ CGameClient& m_gameClient;
+
+ // Initialization parameters
+ RETRO::IStreamManager* m_streamManager = nullptr;
+
+ // Stream parameters
+ std::map<IGameClientStream*, RETRO::StreamPtr> m_streams;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/IGameClientStream.h b/xbmc/games/addons/streams/IGameClientStream.h
new file mode 100644
index 0000000..c2374bc
--- /dev/null
+++ b/xbmc/games/addons/streams/IGameClientStream.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 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
+
+struct game_stream_buffer;
+struct game_stream_packet;
+struct game_stream_properties;
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRetroPlayerStream;
+}
+
+namespace GAME
+{
+
+class IGameClientStream
+{
+public:
+ virtual ~IGameClientStream() = default;
+
+ /*!
+ * \brief Open the stream
+ *
+ * \param stream The RetroPlayer resource to take ownership of
+ *
+ * \return True if the stream was opened, false otherwise
+ */
+ virtual bool OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties) = 0;
+
+ /*!
+ * \brief Release the RetroPlayer stream resource
+ */
+ virtual void CloseStream() = 0;
+
+ /*!
+ * \brief Get a buffer for zero-copy stream data
+ *
+ * \param width The framebuffer width, or 0 for no width specified
+ * \param height The framebuffer height, or 0 for no height specified
+ * \param[out] buffer The buffer, or unmodified if false is returned
+ *
+ * If this returns true, buffer must be freed using ReleaseBuffer().
+ *
+ * \return True if buffer was set, false otherwise
+ */
+ virtual bool GetBuffer(unsigned int width, unsigned int height, game_stream_buffer& buffer)
+ {
+ return false;
+ }
+
+ /*!
+ * \brief Free an allocated buffer
+ *
+ * \param buffer The buffer returned from GetBuffer()
+ */
+ virtual void ReleaseBuffer(game_stream_buffer& buffer) {}
+
+ /*!
+ * \brief Add a data packet to a stream
+ *
+ * \param packet The data packet
+ */
+ virtual void AddData(const game_stream_packet& packet) = 0;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/agents/CMakeLists.txt b/xbmc/games/agents/CMakeLists.txt
new file mode 100644
index 0000000..e66ee2f
--- /dev/null
+++ b/xbmc/games/agents/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GameAgentManager.cpp
+)
+
+set(HEADERS GameAgentManager.h
+)
+
+core_add_library(games_agents)
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(),
+ [&currentPeripheral](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(),
+ [&currentPeripherals, &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, "==========================");
+}
diff --git a/xbmc/games/agents/GameAgentManager.h b/xbmc/games/agents/GameAgentManager.h
new file mode 100644
index 0000000..901a492
--- /dev/null
+++ b/xbmc/games/agents/GameAgentManager.h
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include "games/GameTypes.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+#include "peripherals/PeripheralTypes.h"
+#include "utils/Observer.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+
+class CInputManager;
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+class CPeripherals;
+} // namespace PERIPHERALS
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputProvider;
+}
+
+namespace GAME
+{
+class CGameAgent;
+class CGameClient;
+class CGameClientJoystick;
+
+/*!
+ * \brief Class to manage game-playing agents for a running game client
+ *
+ * Currently, port mapping is controller-based and does not take into account
+ * the human belonging to the controller. In the future, humans and possibly
+ * bots will be managed here.
+ *
+ * To map ports to controllers, a list of controllers is retrieved in
+ * ProcessJoysticks(). After expired controllers are removed, the port mapping
+ * occurs in the static function MapJoysticks(). The strategy is to simply
+ * sort controllers by heuristics and greedily assign to game ports.
+ */
+class CGameAgentManager : public Observable,
+ public Observer,
+ KEYBOARD::IKeyboardDriverHandler,
+ MOUSE::IMouseDriverHandler
+{
+public:
+ CGameAgentManager(PERIPHERALS::CPeripherals& peripheralManager, CInputManager& inputManager);
+
+ virtual ~CGameAgentManager();
+
+ // Lifecycle functions
+ void Start(GameClientPtr gameClient);
+ void Stop();
+ void Refresh();
+
+ // Implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+ // Implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override {}
+
+ // Implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(MOUSE::BUTTON_ID button) override;
+ void OnButtonRelease(MOUSE::BUTTON_ID button) override {}
+
+private:
+ //! @todo De-duplicate these types
+ using PortAddress = std::string;
+ using JoystickMap = std::map<PortAddress, std::shared_ptr<CGameClientJoystick>>;
+ using PortMap = std::map<JOYSTICK::IInputProvider*, std::shared_ptr<CGameClientJoystick>>;
+
+ using PeripheralLocation = std::string;
+ using CurrentPortMap = std::map<PortAddress, PeripheralLocation>;
+ using CurrentPeripheralMap = std::map<PeripheralLocation, PortAddress>;
+
+ using ControllerAddress = std::string;
+ using PeripheralMap = std::map<ControllerAddress, PERIPHERALS::PeripheralPtr>;
+
+ // Internal interface
+ void ProcessJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+ void ProcessKeyboard();
+ void ProcessMouse();
+
+ // Internal helpers
+ void UpdateExpiredJoysticks(const PERIPHERALS::PeripheralVector& joysticks,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+ void UpdateConnectedJoysticks(const PERIPHERALS::PeripheralVector& joysticks,
+ const PortMap& newPortMap,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock,
+ std::set<PERIPHERALS::PeripheralPtr>& disconnectedJoysticks);
+
+ // Static functionals
+ static PortMap MapJoysticks(const PERIPHERALS::PeripheralVector& peripheralJoysticks,
+ const JoystickMap& gameClientjoysticks,
+ CurrentPortMap& currentPorts,
+ CurrentPeripheralMap& currentPeripherals,
+ int playerLimit);
+ static void MapJoystick(PERIPHERALS::PeripheralPtr peripheralJoystick,
+ std::shared_ptr<CGameClientJoystick> gameClientJoystick,
+ PortMap& result);
+ static void LogPeripheralMap(const PeripheralMap& peripheralMap,
+ const std::set<PERIPHERALS::PeripheralPtr>& disconnectedPeripherals);
+
+ // Construction parameters
+ PERIPHERALS::CPeripherals& m_peripheralManager;
+ CInputManager& m_inputManager;
+
+ // State parameters
+ GameClientPtr m_gameClient;
+ bool m_bHasKeyboard = false;
+ bool m_bHasMouse = false;
+
+ /*!
+ * \brief Map of input provider to joystick handler
+ *
+ * The input provider is a handle to agent input.
+ *
+ * The joystick handler connects to joystick input of the game client.
+ *
+ * This property remembers which joysticks are actually being controlled by
+ * agents.
+ *
+ * Not exposed to the game.
+ */
+ PortMap m_portMap;
+
+ /*!
+ * \brief Map of the current ports to their peripheral
+ *
+ * This allows attempt to preserve player numbers.
+ */
+ CurrentPortMap m_currentPorts;
+
+ /*!
+ * \brief Map of the current peripherals to their port
+ *
+ * This allows attempt to preserve player numbers.
+ */
+ CurrentPeripheralMap m_currentPeripherals;
+
+ /*!
+ * Map of controller address to source peripheral
+ *
+ * Source peripherals are not exposed to the game.
+ */
+ PeripheralMap m_peripheralMap;
+
+ /*!
+ * Collection of disconnected joysticks
+ *
+ * Source peripherals are not exposed to the game.
+ */
+ std::set<PERIPHERALS::PeripheralPtr> m_disconnectedPeripherals;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/CMakeLists.txt b/xbmc/games/controllers/CMakeLists.txt
new file mode 100644
index 0000000..b54f5c0
--- /dev/null
+++ b/xbmc/games/controllers/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES Controller.cpp
+ ControllerLayout.cpp
+ ControllerManager.cpp
+ ControllerTranslator.cpp
+ DefaultController.cpp
+)
+
+set(HEADERS Controller.h
+ ControllerDefinitions.h
+ ControllerIDs.h
+ ControllerLayout.h
+ ControllerManager.h
+ ControllerTranslator.h
+ ControllerTypes.h
+ DefaultController.h
+)
+
+core_add_library(games_controller)
diff --git a/xbmc/games/controllers/Controller.cpp b/xbmc/games/controllers/Controller.cpp
new file mode 100644
index 0000000..e51af4e
--- /dev/null
+++ b/xbmc/games/controllers/Controller.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2015-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 "Controller.h"
+
+#include "ControllerDefinitions.h"
+#include "ControllerLayout.h"
+#include "URL.h"
+#include "addons/addoninfo/AddonType.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+// --- FeatureTypeEqual --------------------------------------------------------
+
+struct FeatureTypeEqual
+{
+ FeatureTypeEqual(FEATURE_TYPE type, JOYSTICK::INPUT_TYPE inputType)
+ : type(type), inputType(inputType)
+ {
+ }
+
+ bool operator()(const CPhysicalFeature& feature) const
+ {
+ if (type == FEATURE_TYPE::UNKNOWN)
+ return true; // Match all feature types
+
+ if (type == FEATURE_TYPE::SCALAR && feature.Type() == FEATURE_TYPE::SCALAR)
+ {
+ if (inputType == JOYSTICK::INPUT_TYPE::UNKNOWN)
+ return true; // Match all input types
+
+ return inputType == feature.InputType();
+ }
+
+ return type == feature.Type();
+ }
+
+ const FEATURE_TYPE type;
+ const JOYSTICK::INPUT_TYPE inputType;
+};
+
+// --- CController -------------------------------------------------------------
+
+const ControllerPtr CController::EmptyPtr;
+
+CController::CController(const ADDON::AddonInfoPtr& addonInfo)
+ : CAddon(addonInfo, ADDON::AddonType::GAME_CONTROLLER), m_layout(new CControllerLayout)
+{
+}
+
+CController::~CController() = default;
+
+const CPhysicalFeature& CController::GetFeature(const std::string& name) const
+{
+ auto it =
+ std::find_if(m_features.begin(), m_features.end(),
+ [&name](const CPhysicalFeature& feature) { return name == feature.Name(); });
+
+ if (it != m_features.end())
+ return *it;
+
+ static const CPhysicalFeature invalid{};
+ return invalid;
+}
+
+unsigned int CController::FeatureCount(
+ FEATURE_TYPE type /* = FEATURE_TYPE::UNKNOWN */,
+ JOYSTICK::INPUT_TYPE inputType /* = JOYSTICK::INPUT_TYPE::UNKNOWN */) const
+{
+ auto featureCount =
+ std::count_if(m_features.begin(), m_features.end(), FeatureTypeEqual(type, inputType));
+ return static_cast<unsigned int>(featureCount);
+}
+
+void CController::GetFeatures(std::vector<std::string>& features,
+ FEATURE_TYPE type /* = FEATURE_TYPE::UNKNOWN */) const
+{
+ for (const CPhysicalFeature& feature : m_features)
+ {
+ if (type == FEATURE_TYPE::UNKNOWN || type == feature.Type())
+ features.push_back(feature.Name());
+ }
+}
+
+JOYSTICK::FEATURE_TYPE CController::FeatureType(const std::string& feature) const
+{
+ for (auto it = m_features.begin(); it != m_features.end(); ++it)
+ {
+ if (feature == it->Name())
+ return it->Type();
+ }
+ return JOYSTICK::FEATURE_TYPE::UNKNOWN;
+}
+
+JOYSTICK::INPUT_TYPE CController::GetInputType(const std::string& feature) const
+{
+ for (auto it = m_features.begin(); it != m_features.end(); ++it)
+ {
+ if (feature == it->Name())
+ return it->InputType();
+ }
+ return JOYSTICK::INPUT_TYPE::UNKNOWN;
+}
+
+bool CController::LoadLayout(void)
+{
+ if (!m_bLoaded)
+ {
+ std::string strLayoutXmlPath = LibPath();
+
+ CLog::Log(LOGINFO, "Loading controller layout: {}", CURL::GetRedacted(strLayoutXmlPath));
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(strLayoutXmlPath))
+ {
+ CLog::Log(LOGDEBUG, "Unable to load file: {} at line {}", xmlDoc.ErrorDesc(),
+ xmlDoc.ErrorRow());
+ return false;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || pRootElement->NoChildren() || pRootElement->ValueStr() != LAYOUT_XML_ROOT)
+ {
+ CLog::Log(LOGERROR, "Can't find root <{}> tag", LAYOUT_XML_ROOT);
+ return false;
+ }
+
+ m_layout->Deserialize(pRootElement, this, m_features);
+ if (m_layout->IsValid(true))
+ {
+ m_bLoaded = true;
+ }
+ else
+ {
+ m_layout->Reset();
+ }
+ }
+
+ return m_bLoaded;
+}
+
+const CPhysicalTopology& CController::Topology() const
+{
+ return m_layout->Topology();
+}
diff --git a/xbmc/games/controllers/Controller.h b/xbmc/games/controllers/Controller.h
new file mode 100644
index 0000000..2dd7c18
--- /dev/null
+++ b/xbmc/games/controllers/Controller.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015-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 "ControllerTypes.h"
+#include "addons/Addon.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/joysticks/JoystickTypes.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerLayout;
+class CPhysicalTopology;
+
+using JOYSTICK::FEATURE_TYPE;
+
+class CController : public ADDON::CAddon
+{
+public:
+ explicit CController(const ADDON::AddonInfoPtr& addonInfo);
+
+ ~CController() override;
+
+ static const ControllerPtr EmptyPtr;
+
+ /*!
+ * \brief Get all controller features
+ *
+ * \return The features
+ */
+ const std::vector<CPhysicalFeature>& Features(void) const { return m_features; }
+
+ /*!
+ * \brief Get a feature by its name
+ *
+ * \param name The feature name
+ *
+ * \return The feature, or a feature of type FEATURE_TYPE::UNKNOWN if the name is invalid
+ */
+ const CPhysicalFeature& GetFeature(const std::string& name) const;
+
+ /*!
+ * \brief Get the count of controller features matching the specified types
+ *
+ * \param type The feature type, or FEATURE_TYPE::UNKNOWN to match all feature types
+ * \param inputType The input type, or INPUT_TYPE::UNKNOWN to match all input types
+ *
+ * \return The feature count
+ */
+ unsigned int FeatureCount(FEATURE_TYPE type = FEATURE_TYPE::UNKNOWN,
+ JOYSTICK::INPUT_TYPE inputType = JOYSTICK::INPUT_TYPE::UNKNOWN) const;
+
+ /*!
+ * \brief Get the features matching the specified type
+ *
+ * \param type The feature type, or FEATURE_TYPE::UNKNOWN to get all features
+ */
+ void GetFeatures(std::vector<std::string>& features,
+ FEATURE_TYPE type = FEATURE_TYPE::UNKNOWN) const;
+
+ /*!
+ * \brief Get the type of the specified feature
+ *
+ * \param feature The feature name to look up
+ *
+ * \return The feature type, or FEATURE_TYPE::UNKNOWN if an invalid feature was specified
+ */
+ FEATURE_TYPE FeatureType(const std::string& feature) const;
+
+ /*!
+ * \brief Get the input type of the specified feature
+ *
+ * \param feature The feature name to look up
+ *
+ * \return The input type of the feature, or INPUT_TYPE::UNKNOWN if unknown
+ */
+ JOYSTICK::INPUT_TYPE GetInputType(const std::string& feature) const;
+
+ /*!
+ * \brief Load the controller layout
+ *
+ * \return true if the layout is loaded or was already loaded, false otherwise
+ */
+ bool LoadLayout(void);
+
+ /*!
+ * \brief Get the controller layout
+ */
+ const CControllerLayout& Layout(void) const { return *m_layout; }
+
+ /*!
+ * \brief Get the controller's physical topology
+ *
+ * This defines how controllers physically connect to each other.
+ *
+ * \return The physical topology of the controller
+ */
+ const CPhysicalTopology& Topology() const;
+
+private:
+ std::unique_ptr<CControllerLayout> m_layout;
+ std::vector<CPhysicalFeature> m_features;
+ bool m_bLoaded = false;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerDefinitions.h b/xbmc/games/controllers/ControllerDefinitions.h
new file mode 100644
index 0000000..517eb50
--- /dev/null
+++ b/xbmc/games/controllers/ControllerDefinitions.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015-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
+
+// XML definitions
+#define LAYOUT_XML_ROOT "layout"
+#define LAYOUT_XML_ELM_CATEGORY "category"
+#define LAYOUT_XML_ELM_BUTTON "button"
+#define LAYOUT_XML_ELM_ANALOG_STICK "analogstick"
+#define LAYOUT_XML_ELM_ACCELEROMETER "accelerometer"
+#define LAYOUT_XML_ELM_MOTOR "motor"
+#define LAYOUT_XML_ELM_RELPOINTER "relpointer"
+#define LAYOUT_XML_ELM_ABSPOINTER "abspointer"
+#define LAYOUT_XML_ELM_WHEEL "wheel"
+#define LAYOUT_XML_ELM_THROTTLE "throttle"
+#define LAYOUT_XML_ELM_KEY "key"
+#define LAYOUT_XML_ELM_TOPOLOGY "physicaltopology"
+#define LAYOUT_XML_ELM_PORT "port"
+#define LAYOUT_XML_ELM_ACCEPTS "accepts"
+#define LAYOUT_XML_ATTR_LAYOUT_LABEL "label"
+#define LAYOUT_XML_ATTR_LAYOUT_ICON "icon"
+#define LAYOUT_XML_ATTR_LAYOUT_IMAGE "image"
+#define LAYOUT_XML_ATTR_CATEGORY_NAME "name"
+#define LAYOUT_XML_ATTR_CATEGORY_LABEL "label"
+#define LAYOUT_XML_ATTR_FEATURE_NAME "name"
+#define LAYOUT_XML_ATTR_FEATURE_LABEL "label"
+#define LAYOUT_XML_ATTR_INPUT_TYPE "type"
+#define LAYOUT_XML_ATTR_KEY_SYMBOL "symbol"
+#define LAYOUT_XML_ATTR_PROVIDES_INPUT "providesinput"
+#define LAYOUT_XML_ATTR_PORT_ID "id"
+#define LAYOUT_XML_ATTR_CONTROLLER "controller"
+
+// Controller definitions
+#define FEATURE_CATEGORY_FACE "face"
+#define FEATURE_CATEGORY_SHOULDER "shoulder"
+#define FEATURE_CATEGORY_TRIGGER "triggers"
+#define FEATURE_CATEGORY_ANALOG_STICK "analogsticks"
+#define FEATURE_CATEGORY_ACCELEROMETER "accelerometer"
+#define FEATURE_CATEGORY_HAPTICS "haptics"
+#define FEATURE_CATEGORY_MOUSE_BUTTON "mouse"
+#define FEATURE_CATEGORY_POINTER "pointer"
+#define FEATURE_CATEGORY_LIGHTGUN "lightgun"
+#define FEATURE_CATEGORY_OFFSCREEN "offscreen"
+#define FEATURE_CATEGORY_KEY "keys"
+#define FEATURE_CATEGORY_KEYPAD "keypad"
+#define FEATURE_CATEGORY_HARDWARE "hardware"
+#define FEATURE_CATEGORY_WHEEL "wheel"
+#define FEATURE_CATEGORY_JOYSTICK "joysticks"
+#define FEATURE_CATEGORY_PADDLE "paddles"
diff --git a/xbmc/games/controllers/ControllerIDs.h b/xbmc/games/controllers/ControllerIDs.h
new file mode 100644
index 0000000..a9254e0
--- /dev/null
+++ b/xbmc/games/controllers/ControllerIDs.h
@@ -0,0 +1,15 @@
+/*
+ * 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
+
+// Default controller IDs
+#define DEFAULT_CONTROLLER_ID "game.controller.default"
+#define DEFAULT_KEYBOARD_ID "game.controller.keyboard"
+#define DEFAULT_MOUSE_ID "game.controller.mouse"
+#define DEFAULT_REMOTE_ID "game.controller.remote"
diff --git a/xbmc/games/controllers/ControllerLayout.cpp b/xbmc/games/controllers/ControllerLayout.cpp
new file mode 100644
index 0000000..85816c1
--- /dev/null
+++ b/xbmc/games/controllers/ControllerLayout.cpp
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2015-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 "ControllerLayout.h"
+
+#include "Controller.h"
+#include "ControllerDefinitions.h"
+#include "ControllerTranslator.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerLayout::CControllerLayout() : m_topology(new CPhysicalTopology)
+{
+}
+
+CControllerLayout::CControllerLayout(const CControllerLayout& other)
+ : m_controller(other.m_controller),
+ m_labelId(other.m_labelId),
+ m_icon(other.m_icon),
+ m_strImage(other.m_strImage),
+ m_topology(new CPhysicalTopology(*other.m_topology))
+{
+}
+
+CControllerLayout::~CControllerLayout() = default;
+
+void CControllerLayout::Reset(void)
+{
+ m_controller = nullptr;
+ m_labelId = -1;
+ m_icon.clear();
+ m_strImage.clear();
+ m_topology->Reset();
+}
+
+bool CControllerLayout::IsValid(bool bLog) const
+{
+ if (m_labelId < 0)
+ {
+ if (bLog)
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", LAYOUT_XML_ROOT,
+ LAYOUT_XML_ATTR_LAYOUT_LABEL);
+ return false;
+ }
+
+ if (m_strImage.empty())
+ {
+ if (bLog)
+ CLog::Log(LOGDEBUG, "<{}> tag has no \"{}\" attribute", LAYOUT_XML_ROOT,
+ LAYOUT_XML_ATTR_LAYOUT_IMAGE);
+ return false;
+ }
+
+ return true;
+}
+
+std::string CControllerLayout::Label(void) const
+{
+ std::string label;
+
+ if (m_labelId >= 0 && m_controller != nullptr)
+ label = g_localizeStrings.GetAddonString(m_controller->ID(), m_labelId);
+
+ return label;
+}
+
+std::string CControllerLayout::ImagePath(void) const
+{
+ std::string path;
+
+ if (!m_strImage.empty() && m_controller != nullptr)
+ return URIUtils::AddFileToFolder(URIUtils::GetDirectory(m_controller->LibPath()), m_strImage);
+
+ return path;
+}
+
+void CControllerLayout::Deserialize(const TiXmlElement* pElement,
+ const CController* controller,
+ std::vector<CPhysicalFeature>& features)
+{
+ if (pElement == nullptr || controller == nullptr)
+ return;
+
+ // Controller (used for string lookup and path translation)
+ m_controller = controller;
+
+ // Label
+ std::string strLabel = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_LABEL);
+ if (!strLabel.empty())
+ std::istringstream(strLabel) >> m_labelId;
+
+ // Icon
+ std::string icon = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_ICON);
+ if (!icon.empty())
+ m_icon = icon;
+
+ // Fallback icon, use add-on icon
+ if (m_icon.empty())
+ m_icon = controller->Icon();
+
+ // Image
+ std::string image = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_IMAGE);
+ if (!image.empty())
+ m_strImage = image;
+
+ for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr;
+ pChild = pChild->NextSiblingElement())
+ {
+ if (pChild->ValueStr() == LAYOUT_XML_ELM_CATEGORY)
+ {
+ // Category
+ std::string strCategory = XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CATEGORY_NAME);
+ JOYSTICK::FEATURE_CATEGORY category =
+ CControllerTranslator::TranslateFeatureCategory(strCategory);
+
+ // Category label
+ int categoryLabelId = -1;
+
+ std::string strCategoryLabelId =
+ XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CATEGORY_LABEL);
+ if (!strCategoryLabelId.empty())
+ std::istringstream(strCategoryLabelId) >> categoryLabelId;
+
+ // Features
+ for (const TiXmlElement* pFeature = pChild->FirstChildElement(); pFeature != nullptr;
+ pFeature = pFeature->NextSiblingElement())
+ {
+ CPhysicalFeature feature;
+
+ if (feature.Deserialize(pFeature, controller, category, categoryLabelId))
+ features.push_back(feature);
+ }
+ }
+ else if (pChild->ValueStr() == LAYOUT_XML_ELM_TOPOLOGY)
+ {
+ // Topology
+ CPhysicalTopology topology;
+ if (topology.Deserialize(pChild))
+ *m_topology = std::move(topology);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Ignoring <{}> tag", pChild->ValueStr());
+ }
+ }
+}
diff --git a/xbmc/games/controllers/ControllerLayout.h b/xbmc/games/controllers/ControllerLayout.h
new file mode 100644
index 0000000..1589837
--- /dev/null
+++ b/xbmc/games/controllers/ControllerLayout.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015-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 <memory>
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+class CController;
+class CPhysicalFeature;
+class CPhysicalTopology;
+
+class CControllerLayout
+{
+public:
+ CControllerLayout();
+ CControllerLayout(const CControllerLayout& other);
+ ~CControllerLayout();
+
+ void Reset(void);
+
+ int LabelID(void) const { return m_labelId; }
+ const std::string& Icon(void) const { return m_icon; }
+ const std::string& Image(void) const { return m_strImage; }
+
+ /*!
+ * \brief Ensures the layout was deserialized correctly, and optionally logs if not
+ *
+ * \param bLog If true, output the cause of invalidness to the log
+ *
+ * \return True if the layout is valid and can be used in the GUI, false otherwise
+ */
+ bool IsValid(bool bLog) const;
+
+ /*!
+ * \brief Get the label of the primary layout used when mapping the controller
+ *
+ * \return The label, or empty if unknown
+ */
+ std::string Label(void) const;
+
+ /*!
+ * \brief Get the image path of the primary layout used when mapping the controller
+ *
+ * \return The image path, or empty if unknown
+ */
+ std::string ImagePath(void) const;
+
+ /*!
+ * \brief Get the physical topology of this controller
+ *
+ * The topology of a controller defines its ports and which controllers can
+ * physically be connected to them. Also, the topology defines if the
+ * controller can provide player input, which is false in the case of hubs.
+ *
+ * \return The physical topology of the controller
+ */
+ const CPhysicalTopology& Topology(void) const { return *m_topology; }
+
+ /*!
+ * \brief Deserialize the specified XML element
+ *
+ * \param pLayoutElement The XML element
+ * \param controller The controller, used to obtain read-only properties
+ * \param features The deserialized features, if any
+ */
+ void Deserialize(const TiXmlElement* pLayoutElement,
+ const CController* controller,
+ std::vector<CPhysicalFeature>& features);
+
+private:
+ const CController* m_controller = nullptr;
+ int m_labelId = -1;
+ std::string m_icon;
+ std::string m_strImage;
+ std::unique_ptr<CPhysicalTopology> m_topology;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerManager.cpp b/xbmc/games/controllers/ControllerManager.cpp
new file mode 100644
index 0000000..707733b
--- /dev/null
+++ b/xbmc/games/controllers/ControllerManager.cpp
@@ -0,0 +1,122 @@
+/*
+ * 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 "ControllerManager.h"
+
+#include "Controller.h"
+#include "ControllerIDs.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerManager::CControllerManager(ADDON::CAddonMgr& addonManager)
+ : m_addonManager(addonManager)
+{
+ m_addonManager.Events().Subscribe(this, &CControllerManager::OnEvent);
+}
+
+CControllerManager::~CControllerManager()
+{
+ m_addonManager.Events().Unsubscribe(this);
+}
+
+ControllerPtr CControllerManager::GetController(const std::string& controllerId)
+{
+ using namespace ADDON;
+
+ std::lock_guard<CCriticalSection> lock(m_mutex);
+
+ ControllerPtr& cachedController = m_cache[controllerId];
+
+ if (!cachedController && m_failedControllers.find(controllerId) == m_failedControllers.end())
+ {
+ AddonPtr addon;
+ if (m_addonManager.GetAddon(controllerId, addon, AddonType::GAME_CONTROLLER,
+ OnlyEnabled::CHOICE_NO))
+ cachedController = LoadController(addon);
+ }
+
+ return cachedController;
+}
+
+ControllerPtr CControllerManager::GetDefaultController()
+{
+ return GetController(DEFAULT_CONTROLLER_ID);
+}
+
+ControllerPtr CControllerManager::GetDefaultKeyboard()
+{
+ return GetController(DEFAULT_KEYBOARD_ID);
+}
+
+ControllerPtr CControllerManager::GetDefaultMouse()
+{
+ return GetController(DEFAULT_MOUSE_ID);
+}
+
+ControllerVector CControllerManager::GetControllers()
+{
+ using namespace ADDON;
+
+ ControllerVector controllers;
+
+ std::lock_guard<CCriticalSection> lock(m_mutex);
+
+ VECADDONS addons;
+ if (m_addonManager.GetInstalledAddons(addons, AddonType::GAME_CONTROLLER))
+ {
+ for (auto& addon : addons)
+ {
+ ControllerPtr& cachedController = m_cache[addon->ID()];
+ if (!cachedController && m_failedControllers.find(addon->ID()) == m_failedControllers.end())
+ cachedController = LoadController(addon);
+
+ if (cachedController)
+ controllers.emplace_back(cachedController);
+ }
+ }
+
+ return controllers;
+}
+
+void CControllerManager::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || // Also called on install
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled))
+ {
+ std::lock_guard<CCriticalSection> lock(m_mutex);
+
+ const std::string& addonId = event.addonId;
+
+ // Clear caches for add-on
+ auto it = m_cache.find(addonId);
+ if (it != m_cache.end())
+ m_cache.erase(it);
+
+ auto it2 = m_failedControllers.find(addonId);
+ if (it2 != m_failedControllers.end())
+ m_failedControllers.erase(it2);
+ }
+}
+
+ControllerPtr CControllerManager::LoadController(const ADDON::AddonPtr& addon)
+{
+ ControllerPtr controller = std::static_pointer_cast<CController>(addon);
+ if (!controller->LoadLayout())
+ {
+ m_failedControllers.insert(addon->ID());
+ controller.reset();
+ }
+
+ return controller;
+}
diff --git a/xbmc/games/controllers/ControllerManager.h b/xbmc/games/controllers/ControllerManager.h
new file mode 100644
index 0000000..25ffe0f
--- /dev/null
+++ b/xbmc/games/controllers/ControllerManager.h
@@ -0,0 +1,93 @@
+/*
+ * 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 "ControllerTypes.h"
+#include "addons/IAddon.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <set>
+#include <string>
+
+namespace ADDON
+{
+struct AddonEvent;
+} // namespace ADDON
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerManager
+{
+public:
+ CControllerManager(ADDON::CAddonMgr& addonManager);
+ ~CControllerManager();
+
+ /*!
+ * \brief Get a controller
+ *
+ * A cache is used to avoid reloading controllers each time they are
+ * requested.
+ *
+ * \param controllerId The controller's ID
+ *
+ * \return The controller, or empty if the controller isn't installed or
+ * can't be loaded
+ */
+ ControllerPtr GetController(const std::string& controllerId);
+
+ /*!
+ * \brief Get the default controller
+ *
+ * \return The default controller, or empty if the controller failed to load
+ */
+ ControllerPtr GetDefaultController();
+
+ /*!
+ * \brief Get the default keyboard
+ *
+ * \return The keyboard controller, or empty if the controller failed to load
+ */
+ ControllerPtr GetDefaultKeyboard();
+
+ /*!
+ * \brief Get the default mouse
+ *
+ * \return The mouse controller, or empty if the controller failed to load
+ */
+ ControllerPtr GetDefaultMouse();
+
+ /*!
+ * \brief Get installed controllers
+ *
+ * \return The installed controllers that loaded successfully
+ */
+ ControllerVector GetControllers();
+
+private:
+ // Add-on event handler
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ // Utility functions
+ ControllerPtr LoadController(const ADDON::AddonPtr& addon);
+
+ // Construction parameters
+ ADDON::CAddonMgr& m_addonManager;
+
+ // Controller state
+ std::map<std::string, ControllerPtr> m_cache;
+ std::set<std::string> m_failedControllers; // Controllers that failed to load
+
+ // Synchronization parameters
+ CCriticalSection m_mutex;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerTranslator.cpp b/xbmc/games/controllers/ControllerTranslator.cpp
new file mode 100644
index 0000000..df8641b
--- /dev/null
+++ b/xbmc/games/controllers/ControllerTranslator.cpp
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2015-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 "ControllerTranslator.h"
+
+#include "ControllerDefinitions.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace JOYSTICK;
+
+const char* CControllerTranslator::TranslateFeatureType(FEATURE_TYPE type)
+{
+ switch (type)
+ {
+ case FEATURE_TYPE::SCALAR:
+ return LAYOUT_XML_ELM_BUTTON;
+ case FEATURE_TYPE::ANALOG_STICK:
+ return LAYOUT_XML_ELM_ANALOG_STICK;
+ case FEATURE_TYPE::ACCELEROMETER:
+ return LAYOUT_XML_ELM_ACCELEROMETER;
+ case FEATURE_TYPE::MOTOR:
+ return LAYOUT_XML_ELM_MOTOR;
+ case FEATURE_TYPE::RELPOINTER:
+ return LAYOUT_XML_ELM_RELPOINTER;
+ case FEATURE_TYPE::ABSPOINTER:
+ return LAYOUT_XML_ELM_ABSPOINTER;
+ case FEATURE_TYPE::WHEEL:
+ return LAYOUT_XML_ELM_WHEEL;
+ case FEATURE_TYPE::THROTTLE:
+ return LAYOUT_XML_ELM_THROTTLE;
+ case FEATURE_TYPE::KEY:
+ return LAYOUT_XML_ELM_KEY;
+ default:
+ break;
+ }
+ return "";
+}
+
+FEATURE_TYPE CControllerTranslator::TranslateFeatureType(const std::string& strType)
+{
+ if (strType == LAYOUT_XML_ELM_BUTTON)
+ return FEATURE_TYPE::SCALAR;
+ if (strType == LAYOUT_XML_ELM_ANALOG_STICK)
+ return FEATURE_TYPE::ANALOG_STICK;
+ if (strType == LAYOUT_XML_ELM_ACCELEROMETER)
+ return FEATURE_TYPE::ACCELEROMETER;
+ if (strType == LAYOUT_XML_ELM_MOTOR)
+ return FEATURE_TYPE::MOTOR;
+ if (strType == LAYOUT_XML_ELM_RELPOINTER)
+ return FEATURE_TYPE::RELPOINTER;
+ if (strType == LAYOUT_XML_ELM_ABSPOINTER)
+ return FEATURE_TYPE::ABSPOINTER;
+ if (strType == LAYOUT_XML_ELM_WHEEL)
+ return FEATURE_TYPE::WHEEL;
+ if (strType == LAYOUT_XML_ELM_THROTTLE)
+ return FEATURE_TYPE::THROTTLE;
+ if (strType == LAYOUT_XML_ELM_KEY)
+ return FEATURE_TYPE::KEY;
+
+ return FEATURE_TYPE::UNKNOWN;
+}
+
+const char* CControllerTranslator::TranslateFeatureCategory(FEATURE_CATEGORY category)
+{
+ switch (category)
+ {
+ case FEATURE_CATEGORY::FACE:
+ return FEATURE_CATEGORY_FACE;
+ case FEATURE_CATEGORY::SHOULDER:
+ return FEATURE_CATEGORY_SHOULDER;
+ case FEATURE_CATEGORY::TRIGGER:
+ return FEATURE_CATEGORY_TRIGGER;
+ case FEATURE_CATEGORY::ANALOG_STICK:
+ return FEATURE_CATEGORY_ANALOG_STICK;
+ case FEATURE_CATEGORY::ACCELEROMETER:
+ return FEATURE_CATEGORY_ACCELEROMETER;
+ case FEATURE_CATEGORY::HAPTICS:
+ return FEATURE_CATEGORY_HAPTICS;
+ case FEATURE_CATEGORY::MOUSE_BUTTON:
+ return FEATURE_CATEGORY_MOUSE_BUTTON;
+ case FEATURE_CATEGORY::POINTER:
+ return FEATURE_CATEGORY_POINTER;
+ case FEATURE_CATEGORY::LIGHTGUN:
+ return FEATURE_CATEGORY_LIGHTGUN;
+ case FEATURE_CATEGORY::OFFSCREEN:
+ return FEATURE_CATEGORY_OFFSCREEN;
+ case FEATURE_CATEGORY::KEY:
+ return FEATURE_CATEGORY_KEY;
+ case FEATURE_CATEGORY::KEYPAD:
+ return FEATURE_CATEGORY_KEYPAD;
+ case FEATURE_CATEGORY::HARDWARE:
+ return FEATURE_CATEGORY_HARDWARE;
+ case FEATURE_CATEGORY::WHEEL:
+ return FEATURE_CATEGORY_WHEEL;
+ case FEATURE_CATEGORY::JOYSTICK:
+ return FEATURE_CATEGORY_JOYSTICK;
+ case FEATURE_CATEGORY::PADDLE:
+ return FEATURE_CATEGORY_PADDLE;
+ default:
+ break;
+ }
+ return "";
+}
+
+FEATURE_CATEGORY CControllerTranslator::TranslateFeatureCategory(const std::string& strCategory)
+{
+ if (strCategory == FEATURE_CATEGORY_FACE)
+ return FEATURE_CATEGORY::FACE;
+ if (strCategory == FEATURE_CATEGORY_SHOULDER)
+ return FEATURE_CATEGORY::SHOULDER;
+ if (strCategory == FEATURE_CATEGORY_TRIGGER)
+ return FEATURE_CATEGORY::TRIGGER;
+ if (strCategory == FEATURE_CATEGORY_ANALOG_STICK)
+ return FEATURE_CATEGORY::ANALOG_STICK;
+ if (strCategory == FEATURE_CATEGORY_ACCELEROMETER)
+ return FEATURE_CATEGORY::ACCELEROMETER;
+ if (strCategory == FEATURE_CATEGORY_HAPTICS)
+ return FEATURE_CATEGORY::HAPTICS;
+ if (strCategory == FEATURE_CATEGORY_MOUSE_BUTTON)
+ return FEATURE_CATEGORY::MOUSE_BUTTON;
+ if (strCategory == FEATURE_CATEGORY_POINTER)
+ return FEATURE_CATEGORY::POINTER;
+ if (strCategory == FEATURE_CATEGORY_LIGHTGUN)
+ return FEATURE_CATEGORY::LIGHTGUN;
+ if (strCategory == FEATURE_CATEGORY_OFFSCREEN)
+ return FEATURE_CATEGORY::OFFSCREEN;
+ if (strCategory == FEATURE_CATEGORY_KEY)
+ return FEATURE_CATEGORY::KEY;
+ if (strCategory == FEATURE_CATEGORY_KEYPAD)
+ return FEATURE_CATEGORY::KEYPAD;
+ if (strCategory == FEATURE_CATEGORY_HARDWARE)
+ return FEATURE_CATEGORY::HARDWARE;
+ if (strCategory == FEATURE_CATEGORY_WHEEL)
+ return FEATURE_CATEGORY::WHEEL;
+ if (strCategory == FEATURE_CATEGORY_JOYSTICK)
+ return FEATURE_CATEGORY::JOYSTICK;
+ if (strCategory == FEATURE_CATEGORY_PADDLE)
+ return FEATURE_CATEGORY::PADDLE;
+
+ return FEATURE_CATEGORY::UNKNOWN;
+}
+
+const char* CControllerTranslator::TranslateInputType(INPUT_TYPE type)
+{
+ switch (type)
+ {
+ case INPUT_TYPE::DIGITAL:
+ return "digital";
+ case INPUT_TYPE::ANALOG:
+ return "analog";
+ default:
+ break;
+ }
+ return "";
+}
+
+INPUT_TYPE CControllerTranslator::TranslateInputType(const std::string& strType)
+{
+ if (strType == "digital")
+ return INPUT_TYPE::DIGITAL;
+ if (strType == "analog")
+ return INPUT_TYPE::ANALOG;
+
+ return INPUT_TYPE::UNKNOWN;
+}
+
+KEYBOARD::KeySymbol CControllerTranslator::TranslateKeysym(const std::string& symbol)
+{
+ if (symbol == "backspace")
+ return XBMCK_BACKSPACE;
+ if (symbol == "tab")
+ return XBMCK_TAB;
+ if (symbol == "clear")
+ return XBMCK_CLEAR;
+ if (symbol == "enter")
+ return XBMCK_RETURN;
+ if (symbol == "pause")
+ return XBMCK_PAUSE;
+ if (symbol == "escape")
+ return XBMCK_ESCAPE;
+ if (symbol == "space")
+ return XBMCK_SPACE;
+ if (symbol == "exclaim")
+ return XBMCK_EXCLAIM;
+ if (symbol == "doublequote")
+ return XBMCK_QUOTEDBL;
+ if (symbol == "hash")
+ return XBMCK_HASH;
+ if (symbol == "dollar")
+ return XBMCK_DOLLAR;
+ if (symbol == "ampersand")
+ return XBMCK_AMPERSAND;
+ if (symbol == "quote")
+ return XBMCK_QUOTE;
+ if (symbol == "leftparen")
+ return XBMCK_LEFTPAREN;
+ if (symbol == "rightparen")
+ return XBMCK_RIGHTPAREN;
+ if (symbol == "asterisk")
+ return XBMCK_ASTERISK;
+ if (symbol == "plus")
+ return XBMCK_PLUS;
+ if (symbol == "comma")
+ return XBMCK_COMMA;
+ if (symbol == "minus")
+ return XBMCK_MINUS;
+ if (symbol == "period")
+ return XBMCK_PERIOD;
+ if (symbol == "slash")
+ return XBMCK_SLASH;
+ if (symbol == "0")
+ return XBMCK_0;
+ if (symbol == "1")
+ return XBMCK_1;
+ if (symbol == "2")
+ return XBMCK_2;
+ if (symbol == "3")
+ return XBMCK_3;
+ if (symbol == "4")
+ return XBMCK_4;
+ if (symbol == "5")
+ return XBMCK_5;
+ if (symbol == "6")
+ return XBMCK_6;
+ if (symbol == "7")
+ return XBMCK_7;
+ if (symbol == "8")
+ return XBMCK_8;
+ if (symbol == "9")
+ return XBMCK_9;
+ if (symbol == "colon")
+ return XBMCK_COLON;
+ if (symbol == "semicolon")
+ return XBMCK_SEMICOLON;
+ if (symbol == "less")
+ return XBMCK_LESS;
+ if (symbol == "equals")
+ return XBMCK_EQUALS;
+ if (symbol == "greater")
+ return XBMCK_GREATER;
+ if (symbol == "question")
+ return XBMCK_QUESTION;
+ if (symbol == "at")
+ return XBMCK_AT;
+ if (symbol == "leftbracket")
+ return XBMCK_LEFTBRACKET;
+ if (symbol == "backslash")
+ return XBMCK_BACKSLASH;
+ if (symbol == "rightbracket")
+ return XBMCK_RIGHTBRACKET;
+ if (symbol == "caret")
+ return XBMCK_CARET;
+ if (symbol == "underscore")
+ return XBMCK_UNDERSCORE;
+ if (symbol == "grave")
+ return XBMCK_BACKQUOTE;
+ if (symbol == "a")
+ return XBMCK_a;
+ if (symbol == "b")
+ return XBMCK_b;
+ if (symbol == "c")
+ return XBMCK_c;
+ if (symbol == "d")
+ return XBMCK_d;
+ if (symbol == "e")
+ return XBMCK_e;
+ if (symbol == "f")
+ return XBMCK_f;
+ if (symbol == "g")
+ return XBMCK_g;
+ if (symbol == "h")
+ return XBMCK_h;
+ if (symbol == "i")
+ return XBMCK_i;
+ if (symbol == "j")
+ return XBMCK_j;
+ if (symbol == "k")
+ return XBMCK_k;
+ if (symbol == "l")
+ return XBMCK_l;
+ if (symbol == "m")
+ return XBMCK_m;
+ if (symbol == "n")
+ return XBMCK_n;
+ if (symbol == "o")
+ return XBMCK_o;
+ if (symbol == "p")
+ return XBMCK_p;
+ if (symbol == "q")
+ return XBMCK_q;
+ if (symbol == "r")
+ return XBMCK_r;
+ if (symbol == "s")
+ return XBMCK_s;
+ if (symbol == "t")
+ return XBMCK_t;
+ if (symbol == "u")
+ return XBMCK_u;
+ if (symbol == "v")
+ return XBMCK_v;
+ if (symbol == "w")
+ return XBMCK_w;
+ if (symbol == "x")
+ return XBMCK_x;
+ if (symbol == "y")
+ return XBMCK_y;
+ if (symbol == "z")
+ return XBMCK_z;
+ if (symbol == "leftbrace")
+ return XBMCK_LEFTBRACE;
+ if (symbol == "bar")
+ return XBMCK_PIPE;
+ if (symbol == "rightbrace")
+ return XBMCK_RIGHTBRACE;
+ if (symbol == "tilde")
+ return XBMCK_TILDE;
+ if (symbol == "delete")
+ return XBMCK_DELETE;
+ if (symbol == "kp0")
+ return XBMCK_KP0;
+ if (symbol == "kp1")
+ return XBMCK_KP1;
+ if (symbol == "kp2")
+ return XBMCK_KP2;
+ if (symbol == "kp3")
+ return XBMCK_KP3;
+ if (symbol == "kp4")
+ return XBMCK_KP4;
+ if (symbol == "kp5")
+ return XBMCK_KP5;
+ if (symbol == "kp6")
+ return XBMCK_KP6;
+ if (symbol == "kp7")
+ return XBMCK_KP7;
+ if (symbol == "kp8")
+ return XBMCK_KP8;
+ if (symbol == "kp9")
+ return XBMCK_KP9;
+ if (symbol == "kpperiod")
+ return XBMCK_KP_PERIOD;
+ if (symbol == "kpdivide")
+ return XBMCK_KP_DIVIDE;
+ if (symbol == "kpmultiply")
+ return XBMCK_KP_MULTIPLY;
+ if (symbol == "kpminus")
+ return XBMCK_KP_MINUS;
+ if (symbol == "kpplus")
+ return XBMCK_KP_PLUS;
+ if (symbol == "kpenter")
+ return XBMCK_KP_ENTER;
+ if (symbol == "kpequals")
+ return XBMCK_KP_EQUALS;
+ if (symbol == "up")
+ return XBMCK_UP;
+ if (symbol == "down")
+ return XBMCK_DOWN;
+ if (symbol == "right")
+ return XBMCK_RIGHT;
+ if (symbol == "left")
+ return XBMCK_LEFT;
+ if (symbol == "insert")
+ return XBMCK_INSERT;
+ if (symbol == "home")
+ return XBMCK_HOME;
+ if (symbol == "end")
+ return XBMCK_END;
+ if (symbol == "pageup")
+ return XBMCK_PAGEUP;
+ if (symbol == "pagedown")
+ return XBMCK_PAGEDOWN;
+ if (symbol == "f1")
+ return XBMCK_F1;
+ if (symbol == "f2")
+ return XBMCK_F2;
+ if (symbol == "f3")
+ return XBMCK_F3;
+ if (symbol == "f4")
+ return XBMCK_F4;
+ if (symbol == "f5")
+ return XBMCK_F5;
+ if (symbol == "f6")
+ return XBMCK_F6;
+ if (symbol == "f7")
+ return XBMCK_F7;
+ if (symbol == "f8")
+ return XBMCK_F8;
+ if (symbol == "f9")
+ return XBMCK_F9;
+ if (symbol == "f10")
+ return XBMCK_F10;
+ if (symbol == "f11")
+ return XBMCK_F11;
+ if (symbol == "f12")
+ return XBMCK_F12;
+ if (symbol == "f13")
+ return XBMCK_F13;
+ if (symbol == "f14")
+ return XBMCK_F14;
+ if (symbol == "f15")
+ return XBMCK_F15;
+ if (symbol == "numlock")
+ return XBMCK_NUMLOCK;
+ if (symbol == "capslock")
+ return XBMCK_CAPSLOCK;
+ if (symbol == "scrolllock")
+ return XBMCK_SCROLLOCK;
+ if (symbol == "leftshift")
+ return XBMCK_LSHIFT;
+ if (symbol == "rightshift")
+ return XBMCK_RSHIFT;
+ if (symbol == "leftctrl")
+ return XBMCK_LCTRL;
+ if (symbol == "rightctrl")
+ return XBMCK_RCTRL;
+ if (symbol == "leftalt")
+ return XBMCK_LALT;
+ if (symbol == "rightalt")
+ return XBMCK_RALT;
+ if (symbol == "leftmeta")
+ return XBMCK_LMETA;
+ if (symbol == "rightmeta")
+ return XBMCK_RMETA;
+ if (symbol == "leftsuper")
+ return XBMCK_LSUPER;
+ if (symbol == "rightsuper")
+ return XBMCK_RSUPER;
+ if (symbol == "mode")
+ return XBMCK_MODE;
+ if (symbol == "compose")
+ return XBMCK_COMPOSE;
+ if (symbol == "help")
+ return XBMCK_HELP;
+ if (symbol == "printscreen")
+ return XBMCK_PRINT;
+ if (symbol == "sysreq")
+ return XBMCK_SYSREQ;
+ if (symbol == "break")
+ return XBMCK_BREAK;
+ if (symbol == "menu")
+ return XBMCK_MENU;
+ if (symbol == "power")
+ return XBMCK_POWER;
+ if (symbol == "euro")
+ return XBMCK_EURO;
+ if (symbol == "undo")
+ return XBMCK_UNDO;
+
+ return XBMCK_UNKNOWN;
+}
+
+const char* CControllerTranslator::TranslateKeycode(KEYBOARD::KeySymbol keycode)
+{
+ switch (keycode)
+ {
+ case XBMCK_BACKSPACE:
+ return "backspace";
+ case XBMCK_TAB:
+ return "tab";
+ case XBMCK_CLEAR:
+ return "clear";
+ case XBMCK_RETURN:
+ return "enter";
+ case XBMCK_PAUSE:
+ return "pause";
+ case XBMCK_ESCAPE:
+ return "escape";
+ case XBMCK_SPACE:
+ return "space";
+ case XBMCK_EXCLAIM:
+ return "exclaim";
+ case XBMCK_QUOTEDBL:
+ return "doublequote";
+ case XBMCK_HASH:
+ return "hash";
+ case XBMCK_DOLLAR:
+ return "dollar";
+ case XBMCK_AMPERSAND:
+ return "ampersand";
+ case XBMCK_QUOTE:
+ return "quote";
+ case XBMCK_LEFTPAREN:
+ return "leftparen";
+ case XBMCK_RIGHTPAREN:
+ return "rightparen";
+ case XBMCK_ASTERISK:
+ return "asterisk";
+ case XBMCK_PLUS:
+ return "plus";
+ case XBMCK_COMMA:
+ return "comma";
+ case XBMCK_MINUS:
+ return "minus";
+ case XBMCK_PERIOD:
+ return "period";
+ case XBMCK_SLASH:
+ return "slash";
+ case XBMCK_0:
+ return "0";
+ case XBMCK_1:
+ return "1";
+ case XBMCK_2:
+ return "2";
+ case XBMCK_3:
+ return "3";
+ case XBMCK_4:
+ return "4";
+ case XBMCK_5:
+ return "5";
+ case XBMCK_6:
+ return "6";
+ case XBMCK_7:
+ return "7";
+ case XBMCK_8:
+ return "8";
+ case XBMCK_9:
+ return "9";
+ case XBMCK_COLON:
+ return "colon";
+ case XBMCK_SEMICOLON:
+ return "semicolon";
+ case XBMCK_LESS:
+ return "less";
+ case XBMCK_EQUALS:
+ return "equals";
+ case XBMCK_GREATER:
+ return "greater";
+ case XBMCK_QUESTION:
+ return "question";
+ case XBMCK_AT:
+ return "at";
+ case XBMCK_LEFTBRACKET:
+ return "leftbracket";
+ case XBMCK_BACKSLASH:
+ return "backslash";
+ case XBMCK_RIGHTBRACKET:
+ return "rightbracket";
+ case XBMCK_CARET:
+ return "caret";
+ case XBMCK_UNDERSCORE:
+ return "underscore";
+ case XBMCK_BACKQUOTE:
+ return "grave";
+ case XBMCK_a:
+ return "a";
+ case XBMCK_b:
+ return "b";
+ case XBMCK_c:
+ return "c";
+ case XBMCK_d:
+ return "d";
+ case XBMCK_e:
+ return "e";
+ case XBMCK_f:
+ return "f";
+ case XBMCK_g:
+ return "g";
+ case XBMCK_h:
+ return "h";
+ case XBMCK_i:
+ return "i";
+ case XBMCK_j:
+ return "j";
+ case XBMCK_k:
+ return "k";
+ case XBMCK_l:
+ return "l";
+ case XBMCK_m:
+ return "m";
+ case XBMCK_n:
+ return "n";
+ case XBMCK_o:
+ return "o";
+ case XBMCK_p:
+ return "p";
+ case XBMCK_q:
+ return "q";
+ case XBMCK_r:
+ return "r";
+ case XBMCK_s:
+ return "s";
+ case XBMCK_t:
+ return "t";
+ case XBMCK_u:
+ return "u";
+ case XBMCK_v:
+ return "v";
+ case XBMCK_w:
+ return "w";
+ case XBMCK_x:
+ return "x";
+ case XBMCK_y:
+ return "y";
+ case XBMCK_z:
+ return "z";
+ case XBMCK_LEFTBRACE:
+ return "leftbrace";
+ case XBMCK_PIPE:
+ return "bar";
+ case XBMCK_RIGHTBRACE:
+ return "rightbrace";
+ case XBMCK_TILDE:
+ return "tilde";
+ case XBMCK_DELETE:
+ return "delete";
+ case XBMCK_KP0:
+ return "kp0";
+ case XBMCK_KP1:
+ return "kp1";
+ case XBMCK_KP2:
+ return "kp2";
+ case XBMCK_KP3:
+ return "kp3";
+ case XBMCK_KP4:
+ return "kp4";
+ case XBMCK_KP5:
+ return "kp5";
+ case XBMCK_KP6:
+ return "kp6";
+ case XBMCK_KP7:
+ return "kp7";
+ case XBMCK_KP8:
+ return "kp8";
+ case XBMCK_KP9:
+ return "kp9";
+ case XBMCK_KP_PERIOD:
+ return "kpperiod";
+ case XBMCK_KP_DIVIDE:
+ return "kpdivide";
+ case XBMCK_KP_MULTIPLY:
+ return "kpmultiply";
+ case XBMCK_KP_MINUS:
+ return "kpminus";
+ case XBMCK_KP_PLUS:
+ return "kpplus";
+ case XBMCK_KP_ENTER:
+ return "kpenter";
+ case XBMCK_KP_EQUALS:
+ return "kpequals";
+ case XBMCK_UP:
+ return "up";
+ case XBMCK_DOWN:
+ return "down";
+ case XBMCK_RIGHT:
+ return "right";
+ case XBMCK_LEFT:
+ return "left";
+ case XBMCK_INSERT:
+ return "insert";
+ case XBMCK_HOME:
+ return "home";
+ case XBMCK_END:
+ return "end";
+ case XBMCK_PAGEUP:
+ return "pageup";
+ case XBMCK_PAGEDOWN:
+ return "pagedown";
+ case XBMCK_F1:
+ return "f1";
+ case XBMCK_F2:
+ return "f2";
+ case XBMCK_F3:
+ return "f3";
+ case XBMCK_F4:
+ return "f4";
+ case XBMCK_F5:
+ return "f5";
+ case XBMCK_F6:
+ return "f6";
+ case XBMCK_F7:
+ return "f7";
+ case XBMCK_F8:
+ return "f8";
+ case XBMCK_F9:
+ return "f9";
+ case XBMCK_F10:
+ return "f10";
+ case XBMCK_F11:
+ return "f11";
+ case XBMCK_F12:
+ return "f12";
+ case XBMCK_F13:
+ return "f13";
+ case XBMCK_F14:
+ return "f14";
+ case XBMCK_F15:
+ return "f15";
+ case XBMCK_NUMLOCK:
+ return "numlock";
+ case XBMCK_CAPSLOCK:
+ return "capslock";
+ case XBMCK_SCROLLOCK:
+ return "scrolllock";
+ case XBMCK_LSHIFT:
+ return "leftshift";
+ case XBMCK_RSHIFT:
+ return "rightshift";
+ case XBMCK_LCTRL:
+ return "leftctrl";
+ case XBMCK_RCTRL:
+ return "rightctrl";
+ case XBMCK_LALT:
+ return "leftalt";
+ case XBMCK_RALT:
+ return "rightalt";
+ case XBMCK_LMETA:
+ return "leftmeta";
+ case XBMCK_RMETA:
+ return "rightmeta";
+ case XBMCK_LSUPER:
+ return "leftsuper";
+ case XBMCK_RSUPER:
+ return "rightsuper";
+ case XBMCK_MODE:
+ return "mode";
+ case XBMCK_COMPOSE:
+ return "compose";
+ case XBMCK_HELP:
+ return "help";
+ case XBMCK_PRINT:
+ return "printscreen";
+ case XBMCK_SYSREQ:
+ return "sysreq";
+ case XBMCK_BREAK:
+ return "break";
+ case XBMCK_MENU:
+ return "menu";
+ case XBMCK_POWER:
+ return "power";
+ case XBMCK_EURO:
+ return "euro";
+ case XBMCK_UNDO:
+ return "undo";
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/games/controllers/ControllerTranslator.h b/xbmc/games/controllers/ControllerTranslator.h
new file mode 100644
index 0000000..0bf4bf1
--- /dev/null
+++ b/xbmc/games/controllers/ControllerTranslator.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015-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/joysticks/JoystickTypes.h"
+#include "input/keyboard/KeyboardTypes.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CControllerTranslator
+{
+public:
+ static const char* TranslateFeatureType(JOYSTICK::FEATURE_TYPE type);
+ static JOYSTICK::FEATURE_TYPE TranslateFeatureType(const std::string& strType);
+
+ static const char* TranslateFeatureCategory(JOYSTICK::FEATURE_CATEGORY category);
+ static JOYSTICK::FEATURE_CATEGORY TranslateFeatureCategory(const std::string& strCategory);
+
+ static const char* TranslateInputType(JOYSTICK::INPUT_TYPE type);
+ static JOYSTICK::INPUT_TYPE TranslateInputType(const std::string& strType);
+
+ /*!
+ * \brief Translate a keyboard symbol to a Kodi key code
+ *
+ * \param symbol The key's symbol, defined in the kodi-game-controllers project
+ *
+ * \return The layout-independent keycode associated with the key
+ */
+ static KEYBOARD::KeySymbol TranslateKeysym(const std::string& symbol);
+
+ /*!
+ * \brief Translate a Kodi key code to a keyboard symbol
+ *
+ * \param keycode The Kodi key code
+ *
+ * \return The key's symbol, or an empty string if no symbol is defined for the keycode
+ */
+ static const char* TranslateKeycode(KEYBOARD::KeySymbol keycode);
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerTypes.h b/xbmc/games/controllers/ControllerTypes.h
new file mode 100644
index 0000000..838a07d
--- /dev/null
+++ b/xbmc/games/controllers/ControllerTypes.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015-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 <memory>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CController;
+using ControllerPtr = std::shared_ptr<CController>;
+using ControllerVector = std::vector<ControllerPtr>;
+
+/*!
+ * \brief Type of input provided by a hardware or controller port
+ */
+enum class PORT_TYPE
+{
+ UNKNOWN,
+ KEYBOARD,
+ MOUSE,
+ CONTROLLER,
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/DefaultController.cpp b/xbmc/games/controllers/DefaultController.cpp
new file mode 100644
index 0000000..58bb464
--- /dev/null
+++ b/xbmc/games/controllers/DefaultController.cpp
@@ -0,0 +1,34 @@
+/*
+ * 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 "DefaultController.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const char* CDefaultController::FEATURE_A = "a";
+const char* CDefaultController::FEATURE_B = "b";
+const char* CDefaultController::FEATURE_X = "x";
+const char* CDefaultController::FEATURE_Y = "y";
+const char* CDefaultController::FEATURE_START = "start";
+const char* CDefaultController::FEATURE_BACK = "back";
+const char* CDefaultController::FEATURE_GUIDE = "guide";
+const char* CDefaultController::FEATURE_UP = "up";
+const char* CDefaultController::FEATURE_RIGHT = "right";
+const char* CDefaultController::FEATURE_DOWN = "down";
+const char* CDefaultController::FEATURE_LEFT = "left";
+const char* CDefaultController::FEATURE_LEFT_THUMB = "leftthumb";
+const char* CDefaultController::FEATURE_RIGHT_THUMB = "rightthumb";
+const char* CDefaultController::FEATURE_LEFT_BUMPER = "leftbumper";
+const char* CDefaultController::FEATURE_RIGHT_BUMPER = "rightbumper";
+const char* CDefaultController::FEATURE_LEFT_TRIGGER = "lefttrigger";
+const char* CDefaultController::FEATURE_RIGHT_TRIGGER = "righttrigger";
+const char* CDefaultController::FEATURE_LEFT_STICK = "leftstick";
+const char* CDefaultController::FEATURE_RIGHT_STICK = "rightstick";
+const char* CDefaultController::FEATURE_LEFT_MOTOR = "leftmotor";
+const char* CDefaultController::FEATURE_RIGHT_MOTOR = "rightmotor";
diff --git a/xbmc/games/controllers/DefaultController.h b/xbmc/games/controllers/DefaultController.h
new file mode 100644
index 0000000..f82ce49
--- /dev/null
+++ b/xbmc/games/controllers/DefaultController.h
@@ -0,0 +1,50 @@
+/*
+ * 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
+
+namespace KODI
+{
+namespace GAME
+{
+class CDefaultController
+{
+public:
+ // Face buttons
+ static const char* FEATURE_A;
+ static const char* FEATURE_B;
+ static const char* FEATURE_X;
+ static const char* FEATURE_Y;
+ static const char* FEATURE_START;
+ static const char* FEATURE_BACK;
+ static const char* FEATURE_GUIDE;
+ static const char* FEATURE_UP;
+ static const char* FEATURE_RIGHT;
+ static const char* FEATURE_DOWN;
+ static const char* FEATURE_LEFT;
+ static const char* FEATURE_LEFT_THUMB;
+ static const char* FEATURE_RIGHT_THUMB;
+
+ // Shoulder buttons
+ static const char* FEATURE_LEFT_BUMPER;
+ static const char* FEATURE_RIGHT_BUMPER;
+
+ // Triggers
+ static const char* FEATURE_LEFT_TRIGGER;
+ static const char* FEATURE_RIGHT_TRIGGER;
+
+ // Analog sticks
+ static const char* FEATURE_LEFT_STICK;
+ static const char* FEATURE_RIGHT_STICK;
+
+ // Haptics
+ static const char* FEATURE_LEFT_MOTOR;
+ static const char* FEATURE_RIGHT_MOTOR;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/CMakeLists.txt b/xbmc/games/controllers/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..e40e60e
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES ControllerInstaller.cpp
+ ControllerSelect.cpp
+ GUIDialogAxisDetection.cpp
+ GUIDialogButtonCapture.cpp
+ GUIDialogIgnoreInput.cpp
+)
+
+set(HEADERS ControllerInstaller.h
+ ControllerSelect.h
+ GUIDialogAxisDetection.h
+ GUIDialogButtonCapture.h
+ GUIDialogIgnoreInput.h
+)
+
+core_add_library(games_controller_dialogs)
diff --git a/xbmc/games/controllers/dialogs/ControllerInstaller.cpp b/xbmc/games/controllers/dialogs/ControllerInstaller.cpp
new file mode 100644
index 0000000..9b0fe6f
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerInstaller.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 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 "ControllerInstaller.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerInstaller::CControllerInstaller() : CThread("ControllerInstaller")
+{
+}
+
+void CControllerInstaller::Process()
+{
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui == nullptr)
+ return;
+
+ CGUIWindowManager& windowManager = gui->GetWindowManager();
+
+ auto pSelectDialog = windowManager.GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pSelectDialog == nullptr)
+ return;
+
+ auto pProgressDialog = windowManager.GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (pProgressDialog == nullptr)
+ return;
+
+ ADDON::VECADDONS installableAddons;
+ CServiceBroker::GetAddonMgr().GetInstallableAddons(installableAddons,
+ ADDON::AddonType::GAME_CONTROLLER);
+ if (installableAddons.empty())
+ {
+ // "Controller profiles"
+ // "All available controller profiles are installed."
+ MESSAGING::HELPERS::ShowOKDialogText(CVariant{35050}, CVariant{35062});
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "Controller installer: Found {} controller add-ons",
+ installableAddons.size());
+
+ CFileItemList items;
+ for (const auto& addon : installableAddons)
+ {
+ CFileItemPtr item(new CFileItem(addon->Name()));
+ item->SetArt("icon", addon->Icon());
+ items.Add(std::move(item));
+ }
+
+ pSelectDialog->Reset();
+ pSelectDialog->SetHeading(39020); // "The following additional add-ons will be installed"
+ pSelectDialog->SetUseDetails(true);
+ pSelectDialog->EnableButton(true, 186); // "OK""
+ for (const auto& it : items)
+ pSelectDialog->Add(*it);
+ pSelectDialog->Open();
+
+ if (!pSelectDialog->IsButtonPressed())
+ {
+ CLog::Log(LOGDEBUG, "Controller installer: User cancelled installation dialog");
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "Controller installer: Installing {} controller add-ons",
+ installableAddons.size());
+
+ pProgressDialog->SetHeading(CVariant{24086}); // "Installing add-on..."
+ pProgressDialog->SetLine(0, CVariant{""});
+ pProgressDialog->SetLine(1, CVariant{""});
+ pProgressDialog->SetLine(2, CVariant{""});
+
+ pProgressDialog->Open();
+
+ unsigned int installedCount = 0;
+ while (installedCount < installableAddons.size())
+ {
+ const auto& addon = installableAddons[installedCount];
+
+ // Set dialog text
+ const std::string& progressTemplate = g_localizeStrings.Get(24057); // "Installing {0:s}..."
+ const std::string progressText = StringUtils::Format(progressTemplate, addon->Name());
+ pProgressDialog->SetLine(0, CVariant{progressText});
+
+ // Set dialog percentage
+ const unsigned int percentage =
+ 100 * (installedCount + 1) / static_cast<unsigned int>(installableAddons.size());
+ pProgressDialog->SetPercentage(percentage);
+
+ if (!ADDON::CAddonInstaller::GetInstance().InstallOrUpdate(
+ addon->ID(), ADDON::BackgroundJob::CHOICE_NO, ADDON::ModalJob::CHOICE_NO))
+ {
+ CLog::Log(LOGERROR, "Controller installer: Failed to install {}", addon->ID());
+ // "Error"
+ // "Failed to install add-on."
+ MESSAGING::HELPERS::ShowOKDialogText(257, 35256);
+ break;
+ }
+
+ if (pProgressDialog->IsCanceled())
+ {
+ CLog::Log(LOGDEBUG, "Controller installer: User cancelled add-on installation");
+ break;
+ }
+
+ if (windowManager.GetActiveWindowOrDialog() != WINDOW_DIALOG_PROGRESS)
+ {
+ CLog::Log(LOGDEBUG, "Controller installer: Progress dialog is hidden, cancelling");
+ break;
+ }
+
+ installedCount++;
+ }
+
+ CLog::Log(LOGDEBUG, "Controller window: Installed {} controller add-ons", installedCount);
+ pProgressDialog->Close();
+}
diff --git a/xbmc/games/controllers/dialogs/ControllerInstaller.h b/xbmc/games/controllers/dialogs/ControllerInstaller.h
new file mode 100644
index 0000000..9863e27
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerInstaller.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 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 "threads/Thread.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerInstaller : public CThread
+{
+public:
+ CControllerInstaller();
+ ~CControllerInstaller() override = default;
+
+protected:
+ // implementation of CThread
+ void Process() override;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/ControllerSelect.cpp b/xbmc/games/controllers/dialogs/ControllerSelect.cpp
new file mode 100644
index 0000000..2ef9941
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerSelect.cpp
@@ -0,0 +1,133 @@
+/*
+ * 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 "ControllerSelect.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/ControllerTypes.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerSelect::CControllerSelect() : CThread("ControllerSelect")
+{
+}
+
+CControllerSelect::~CControllerSelect() = default;
+
+void CControllerSelect::Initialize(ControllerVector controllers,
+ ControllerPtr defaultController,
+ bool showDisconnect,
+ const std::function<void(ControllerPtr)>& callback)
+{
+ // Validate parameters
+ if (!callback)
+ return;
+
+ // Stop thread and reset state
+ Deinitialize();
+
+ // Initialize state
+ m_controllers = std::move(controllers);
+ m_defaultController = std::move(defaultController);
+ m_showDisconnect = showDisconnect;
+ m_callback = callback;
+
+ // Create thread
+ Create(false);
+}
+
+void CControllerSelect::Deinitialize()
+{
+ // Stop thread
+ StopThread(true);
+
+ // Reset state
+ m_controllers.clear();
+ m_defaultController.reset();
+ m_showDisconnect = true;
+ m_callback = nullptr;
+}
+
+void CControllerSelect::Process()
+{
+ // Select first controller by default
+ unsigned int initialSelected = 0;
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui == nullptr)
+ return;
+
+ CGUIWindowManager& windowManager = gui->GetWindowManager();
+
+ auto pSelectDialog = windowManager.GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pSelectDialog == nullptr)
+ return;
+
+ CLog::Log(LOGDEBUG, "Controller select: Showing dialog for {} controllers", m_controllers.size());
+
+ CFileItemList items;
+ for (const ControllerPtr& controller : m_controllers)
+ {
+ CFileItemPtr item(new CFileItem(controller->Layout().Label()));
+ item->SetArt("icon", controller->Layout().ImagePath());
+ items.Add(std::move(item));
+
+ // Check if a specified controller should be selected by default
+ if (m_defaultController && m_defaultController->ID() == controller->ID())
+ initialSelected = items.Size() - 1;
+ }
+
+ if (m_showDisconnect)
+ {
+ // Add a button to disconnect the port
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(13298))); // "Disconnected"
+ item->SetArt("icon", "DefaultAddonNone.png");
+ items.Add(std::move(item));
+
+ // Check if the disconnect button should be selected by default
+ if (!m_defaultController)
+ initialSelected = items.Size() - 1;
+ }
+
+ pSelectDialog->Reset();
+ pSelectDialog->SetHeading(CVariant{35113}); // "Select a Controller"
+ pSelectDialog->SetUseDetails(true);
+ pSelectDialog->EnableButton(false, 186); // "OK""
+ pSelectDialog->SetButtonFocus(false);
+ for (const auto& it : items)
+ pSelectDialog->Add(*it);
+ pSelectDialog->SetSelected(static_cast<int>(initialSelected));
+ pSelectDialog->Open();
+
+ // If the thread was stopped, exit early
+ if (m_bStop)
+ return;
+
+ if (pSelectDialog->IsConfirmed())
+ {
+ ControllerPtr resultController;
+
+ const int selected = pSelectDialog->GetSelectedItem();
+ if (0 <= selected && selected < static_cast<int>(m_controllers.size()))
+ resultController = m_controllers.at(selected);
+
+ // Fire a callback with the result
+ m_callback(resultController);
+ }
+}
diff --git a/xbmc/games/controllers/dialogs/ControllerSelect.h b/xbmc/games/controllers/dialogs/ControllerSelect.h
new file mode 100644
index 0000000..83a1b41
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerSelect.h
@@ -0,0 +1,44 @@
+/*
+ * 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/ControllerTypes.h"
+#include "threads/Thread.h"
+
+#include <functional>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerSelect : public CThread
+{
+public:
+ CControllerSelect();
+ ~CControllerSelect() override;
+
+ void Initialize(ControllerVector controllers,
+ ControllerPtr defaultController,
+ bool showDisconnect,
+ const std::function<void(ControllerPtr)>& callback);
+ void Deinitialize();
+
+protected:
+ // Implementation of CThread
+ void Process() override;
+
+private:
+ // State parameters
+ ControllerVector m_controllers;
+ ControllerPtr m_defaultController;
+ bool m_showDisconnect = true;
+ std::function<void(ControllerPtr)> m_callback;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp
new file mode 100644
index 0000000..2052a4c
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp
@@ -0,0 +1,84 @@
+/*
+ * 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 "GUIDialogAxisDetection.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+std::string CGUIDialogAxisDetection::GetDialogText()
+{
+ // "Press all analog buttons now to detect them:[CR][CR]%s"
+ const std::string& dialogText = g_localizeStrings.Get(35020);
+
+ std::vector<std::string> primitives;
+
+ for (const auto& axisEntry : m_detectedAxes)
+ {
+ JOYSTICK::CDriverPrimitive axis(axisEntry.second, 0, JOYSTICK::SEMIAXIS_DIRECTION::POSITIVE, 1);
+ primitives.emplace_back(JOYSTICK::CJoystickTranslator::GetPrimitiveName(axis));
+ }
+
+ return StringUtils::Format(dialogText, StringUtils::Join(primitives, " | "));
+}
+
+std::string CGUIDialogAxisDetection::GetDialogHeader()
+{
+ return g_localizeStrings.Get(35058); // "Controller Configuration"
+}
+
+bool CGUIDialogAxisDetection::MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ if (primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS)
+ AddAxis(buttonMap->Location(), primitive.Index());
+
+ return true;
+}
+
+bool CGUIDialogAxisDetection::AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const
+{
+ switch (type)
+ {
+ case JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS:
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+void CGUIDialogAxisDetection::OnLateAxis(const JOYSTICK::IButtonMap* buttonMap,
+ unsigned int axisIndex)
+{
+ AddAxis(buttonMap->Location(), axisIndex);
+}
+
+void CGUIDialogAxisDetection::AddAxis(const std::string& deviceLocation, unsigned int axisIndex)
+{
+ auto it = std::find_if(m_detectedAxes.begin(), m_detectedAxes.end(),
+ [&deviceLocation, axisIndex](const AxisEntry& axis) {
+ return axis.first == deviceLocation && axis.second == axisIndex;
+ });
+
+ if (it == m_detectedAxes.end())
+ {
+ m_detectedAxes.emplace_back(std::make_pair(deviceLocation, axisIndex));
+ m_captureEvent.Set();
+ }
+}
diff --git a/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h
new file mode 100644
index 0000000..4ca0d27
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "GUIDialogButtonCapture.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogAxisDetection : public CGUIDialogButtonCapture
+{
+public:
+ CGUIDialogAxisDetection() = default;
+
+ ~CGUIDialogAxisDetection() override = default;
+
+ // specialization of IButtonMapper via CGUIDialogButtonCapture
+ bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override;
+ void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override;
+
+protected:
+ // implementation of CGUIDialogButtonCapture
+ std::string GetDialogText() override;
+ std::string GetDialogHeader() override;
+ bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnClose(bool bAccepted) override {}
+
+private:
+ void AddAxis(const std::string& deviceLocation, unsigned int axisIndex);
+
+ // Axis types
+ using DeviceName = std::string;
+ using AxisIndex = unsigned int;
+ using AxisEntry = std::pair<DeviceName, AxisIndex>;
+ using AxisVector = std::vector<AxisEntry>;
+
+ // Axis detection
+ AxisVector m_detectedAxes;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp
new file mode 100644
index 0000000..9bf8dbc
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2016-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 "GUIDialogButtonCapture.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/ControllerIDs.h"
+#include "input/IKeymap.h"
+#include "input/actions/ActionIDs.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "peripherals/Peripherals.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace KODI;
+using namespace GAME;
+using namespace KODI::MESSAGING;
+
+CGUIDialogButtonCapture::CGUIDialogButtonCapture() : CThread("ButtonCaptureDlg")
+{
+}
+
+std::string CGUIDialogButtonCapture::ControllerID(void) const
+{
+ return DEFAULT_CONTROLLER_ID;
+}
+
+void CGUIDialogButtonCapture::Show()
+{
+ if (!IsRunning())
+ {
+ InstallHooks();
+
+ Create();
+
+ bool bAccepted =
+ HELPERS::ShowOKDialogText(CVariant{GetDialogHeader()}, CVariant{GetDialogText()});
+
+ StopThread(false);
+
+ m_captureEvent.Set();
+
+ OnClose(bAccepted);
+
+ RemoveHooks();
+ }
+}
+
+void CGUIDialogButtonCapture::Process()
+{
+ while (!m_bStop)
+ {
+ m_captureEvent.Wait();
+
+ if (m_bStop)
+ break;
+
+ //! @todo Move to rendering thread when there is a rendering thread
+ HELPERS::UpdateOKDialogText(CVariant{35013}, CVariant{GetDialogText()});
+ }
+}
+
+bool CGUIDialogButtonCapture::MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ if (m_bStop)
+ return false;
+
+ // First check to see if driver primitive closes the dialog
+ if (keymap && keymap->ControllerID() == buttonMap->ControllerID())
+ {
+ std::string feature;
+ if (buttonMap->GetFeature(primitive, feature))
+ {
+ const auto& actions =
+ keymap->GetActions(JOYSTICK::CJoystickUtils::MakeKeyName(feature)).actions;
+ if (!actions.empty())
+ {
+ switch (actions.begin()->actionId)
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_NAV_BACK:
+ case ACTION_PREVIOUS_MENU:
+ return false;
+ default:
+ break;
+ }
+ }
+ }
+ }
+
+ return MapPrimitiveInternal(buttonMap, keymap, primitive);
+}
+
+void CGUIDialogButtonCapture::InstallHooks(void)
+{
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+ CServiceBroker::GetPeripherals().RegisterObserver(this);
+}
+
+void CGUIDialogButtonCapture::RemoveHooks(void)
+{
+ CServiceBroker::GetPeripherals().UnregisterObserver(this);
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+}
+
+void CGUIDialogButtonCapture::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessagePeripheralsChanged:
+ {
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h
new file mode 100644
index 0000000..97795c6
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016-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/joysticks/interfaces/IButtonMapper.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/Observer.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogButtonCapture : public JOYSTICK::IButtonMapper, public Observer, protected CThread
+{
+public:
+ CGUIDialogButtonCapture();
+
+ ~CGUIDialogButtonCapture() override = default;
+
+ // implementation of IButtonMapper
+ std::string ControllerID() const override;
+ bool NeedsCooldown() const override { return false; }
+ bool MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion) override {}
+ void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override {}
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+ /*!
+ * \brief Show the dialog
+ */
+ void Show();
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+ virtual std::string GetDialogText() = 0;
+ virtual std::string GetDialogHeader() = 0;
+ virtual bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) = 0;
+ virtual void OnClose(bool bAccepted) = 0;
+
+ CEvent m_captureEvent;
+
+private:
+ void InstallHooks();
+ void RemoveHooks();
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp
new file mode 100644
index 0000000..b5f28d6
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp
@@ -0,0 +1,132 @@
+/*
+ * 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 "GUIDialogIgnoreInput.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IButtonMapCallback.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGUIDialogIgnoreInput::AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const
+{
+ switch (type)
+ {
+ case JOYSTICK::PRIMITIVE_TYPE::BUTTON:
+ case JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS:
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+std::string CGUIDialogIgnoreInput::GetDialogText()
+{
+ // "Some controllers have buttons and axes that interfere with mapping. Press
+ // these now to disable them:[CR]%s"
+ const std::string& dialogText = g_localizeStrings.Get(35014);
+
+ std::vector<std::string> primitives;
+
+ std::transform(m_capturedPrimitives.begin(), m_capturedPrimitives.end(),
+ std::back_inserter(primitives), [](const JOYSTICK::CDriverPrimitive& primitive) {
+ return JOYSTICK::CJoystickTranslator::GetPrimitiveName(primitive);
+ });
+
+ return StringUtils::Format(dialogText, StringUtils::Join(primitives, " | "));
+}
+
+std::string CGUIDialogIgnoreInput::GetDialogHeader()
+{
+
+ return g_localizeStrings.Get(35019); // "Ignore input"
+}
+
+bool CGUIDialogIgnoreInput::MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ // Check if we have already started capturing primitives for a device
+ const bool bHasDevice = !m_location.empty();
+
+ // If a primitive comes from a different device, ignore it
+ if (bHasDevice && m_location != buttonMap->Location())
+ {
+ CLog::Log(LOGDEBUG, "{}: ignoring input from device {}", buttonMap->ControllerID(),
+ buttonMap->Location());
+ return false;
+ }
+
+ if (!bHasDevice)
+ {
+ CLog::Log(LOGDEBUG, "{}: capturing input for device {}", buttonMap->ControllerID(),
+ buttonMap->Location());
+ m_location = buttonMap->Location();
+ }
+
+ if (AddPrimitive(primitive))
+ {
+ buttonMap->SetIgnoredPrimitives(m_capturedPrimitives);
+ m_captureEvent.Set();
+ }
+
+ return true;
+}
+
+void CGUIDialogIgnoreInput::OnClose(bool bAccepted)
+{
+ for (auto& callback : ButtonMapCallbacks())
+ {
+ if (bAccepted)
+ {
+ // See documentation of IButtonMapCallback::ResetIgnoredPrimitives()
+ // for why this call is needed
+ if (m_location.empty())
+ callback.second->ResetIgnoredPrimitives();
+
+ if (m_location.empty() || m_location == callback.first)
+ callback.second->SaveButtonMap();
+ }
+ else
+ callback.second->RevertButtonMap();
+ }
+}
+
+bool CGUIDialogIgnoreInput::AddPrimitive(const JOYSTICK::CDriverPrimitive& primitive)
+{
+ bool bValid = false;
+
+ if (primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::BUTTON ||
+ primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS)
+ {
+ auto PrimitiveMatch = [&primitive](const JOYSTICK::CDriverPrimitive& other) {
+ return primitive.Type() == other.Type() && primitive.Index() == other.Index();
+ };
+
+ bValid = std::find_if(m_capturedPrimitives.begin(), m_capturedPrimitives.end(),
+ PrimitiveMatch) == m_capturedPrimitives.end();
+ }
+
+ if (bValid)
+ {
+ m_capturedPrimitives.emplace_back(primitive);
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h
new file mode 100644
index 0000000..e1d3922
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "GUIDialogButtonCapture.h"
+#include "input/joysticks/DriverPrimitive.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogIgnoreInput : public CGUIDialogButtonCapture
+{
+public:
+ CGUIDialogIgnoreInput() = default;
+
+ ~CGUIDialogIgnoreInput() override = default;
+
+ // specialization of IButtonMapper via CGUIDialogButtonCapture
+ bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override;
+
+protected:
+ // implementation of CGUIDialogButtonCapture
+ std::string GetDialogText() override;
+ std::string GetDialogHeader() override;
+ bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnClose(bool bAccepted) override;
+
+private:
+ bool AddPrimitive(const JOYSTICK::CDriverPrimitive& primitive);
+
+ std::string m_location;
+ std::vector<JOYSTICK::CDriverPrimitive> m_capturedPrimitives;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/CMakeLists.txt b/xbmc/games/controllers/guicontrols/CMakeLists.txt
new file mode 100644
index 0000000..e6babcc
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/CMakeLists.txt
@@ -0,0 +1,28 @@
+set(SOURCES GUICardinalFeatureButton.cpp
+ GUIControllerButton.cpp
+ GUIFeatureButton.cpp
+ GUIFeatureControls.cpp
+ GUIFeatureFactory.cpp
+ GUIFeatureTranslator.cpp
+ GUIGameController.cpp
+ GUIScalarFeatureButton.cpp
+ GUISelectKeyButton.cpp
+ GUIThrottleButton.cpp
+ GUIWheelButton.cpp
+)
+
+set(HEADERS GUICardinalFeatureButton.h
+ GUIControllerButton.h
+ GUIControlTypes.h
+ GUIFeatureButton.h
+ GUIFeatureControls.h
+ GUIFeatureFactory.h
+ GUIFeatureTranslator.h
+ GUIGameController.h
+ GUIScalarFeatureButton.h
+ GUISelectKeyButton.h
+ GUIThrottleButton.h
+ GUIWheelButton.h
+)
+
+core_add_library(games_controller_guicontrols)
diff --git a/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp
new file mode 100644
index 0000000..3c8732c
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016-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 "GUICardinalFeatureButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUICardinalFeatureButton::CGUICardinalFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUICardinalFeatureButton::PromptForInput(CEvent& waitEvent)
+{
+ using namespace JOYSTICK;
+
+ bool bInterrupted = false;
+
+ // Get the prompt for the current analog stick direction
+ std::string strPrompt;
+ std::string strWarn;
+ switch (m_state)
+ {
+ case STATE::CARDINAL_DIRECTION_UP:
+ strPrompt = g_localizeStrings.Get(35092); // "Move %s up"
+ strWarn = g_localizeStrings.Get(35093); // "Move %s up (%d)"
+ break;
+ case STATE::CARDINAL_DIRECTION_RIGHT:
+ strPrompt = g_localizeStrings.Get(35096); // "Move %s right"
+ strWarn = g_localizeStrings.Get(35097); // "Move %s right (%d)"
+ break;
+ case STATE::CARDINAL_DIRECTION_DOWN:
+ strPrompt = g_localizeStrings.Get(35094); // "Move %s down"
+ strWarn = g_localizeStrings.Get(35095); // "Move %s down (%d)"
+ break;
+ case STATE::CARDINAL_DIRECTION_LEFT:
+ strPrompt = g_localizeStrings.Get(35098); // "Move %s left"
+ strWarn = g_localizeStrings.Get(35099); // "Move %s left (%d)"
+ break;
+ default:
+ break;
+ }
+
+ if (!strPrompt.empty())
+ {
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ if (!bInterrupted)
+ m_state = STATE::FINISHED; // Not interrupted, must have timed out
+ else
+ m_state = GetNextState(m_state); // Interrupted by input, proceed
+ }
+
+ return bInterrupted;
+}
+
+bool CGUICardinalFeatureButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+KODI::INPUT::CARDINAL_DIRECTION CGUICardinalFeatureButton::GetCardinalDirection(void) const
+{
+ using namespace INPUT;
+
+ switch (m_state)
+ {
+ case STATE::CARDINAL_DIRECTION_UP:
+ return CARDINAL_DIRECTION::UP;
+ case STATE::CARDINAL_DIRECTION_RIGHT:
+ return CARDINAL_DIRECTION::RIGHT;
+ case STATE::CARDINAL_DIRECTION_DOWN:
+ return CARDINAL_DIRECTION::DOWN;
+ case STATE::CARDINAL_DIRECTION_LEFT:
+ return CARDINAL_DIRECTION::LEFT;
+ default:
+ break;
+ }
+
+ return CARDINAL_DIRECTION::NONE;
+}
+
+void CGUICardinalFeatureButton::Reset(void)
+{
+ m_state = STATE::CARDINAL_DIRECTION_UP;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h
new file mode 100644
index 0000000..48eb0e8
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUICardinalFeatureButton : public CGUIFeatureButton
+{
+public:
+ CGUICardinalFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUICardinalFeatureButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ INPUT::CARDINAL_DIRECTION GetCardinalDirection() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ CARDINAL_DIRECTION_UP,
+ CARDINAL_DIRECTION_RIGHT,
+ CARDINAL_DIRECTION_DOWN,
+ CARDINAL_DIRECTION_LEFT,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+
+using CGUIAnalogStickButton = CGUICardinalFeatureButton;
+using CGUIRelativePointerButton = CGUICardinalFeatureButton;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIControlTypes.h b/xbmc/games/controllers/guicontrols/GUIControlTypes.h
new file mode 100644
index 0000000..bf91b6e
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIControlTypes.h
@@ -0,0 +1,29 @@
+/*
+ * 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
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Types of button controls that can populate the feature list
+ */
+enum class BUTTON_TYPE
+{
+ UNKNOWN,
+ BUTTON,
+ ANALOG_STICK,
+ RELATIVE_POINTER,
+ WHEEL,
+ THROTTLE,
+ SELECT_KEY,
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp b/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp
new file mode 100644
index 0000000..c8156ce
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014-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 "GUIControllerButton.h"
+
+#include "games/controllers/windows/GUIControllerDefines.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIControllerButton::CGUIControllerButton(const CGUIButtonControl& buttonControl,
+ const std::string& label,
+ unsigned int index)
+ : CGUIButtonControl(buttonControl)
+{
+ // Initialize CGUIButtonControl
+ SetLabel(label);
+ SetID(CONTROL_CONTROLLER_BUTTONS_START + index);
+ SetVisible(true);
+ AllocResources();
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIControllerButton.h b/xbmc/games/controllers/guicontrols/GUIControllerButton.h
new file mode 100644
index 0000000..ebdc924
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIControllerButton.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016-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 "guilib/GUIButtonControl.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIControllerButton : public CGUIButtonControl
+{
+public:
+ CGUIControllerButton(const CGUIButtonControl& buttonControl,
+ const std::string& label,
+ unsigned int index);
+
+ ~CGUIControllerButton() override = default;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp
new file mode 100644
index 0000000..0f1bfd6
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/windows/GUIControllerDefines.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "threads/Event.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace std::chrono_literals;
+
+CGUIFeatureButton::CGUIFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIButtonControl(buttonTemplate), m_feature(feature), m_wizard(wizard)
+{
+ // Initialize CGUIButtonControl
+ SetLabel(m_feature.Label());
+ SetID(CONTROL_FEATURE_BUTTONS_START + index);
+ SetVisible(true);
+ AllocResources();
+}
+
+void CGUIFeatureButton::OnUnFocus(void)
+{
+ CGUIButtonControl::OnUnFocus();
+ m_wizard->OnUnfocus(this);
+}
+
+bool CGUIFeatureButton::DoPrompt(const std::string& strPrompt,
+ const std::string& strWarn,
+ const std::string& strFeature,
+ CEvent& waitEvent)
+{
+ bool bInterrupted = false;
+
+ if (!HasFocus())
+ {
+ CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), GetID());
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msgFocus, WINDOW_INVALID, false);
+ }
+
+ CGUIMessage msgLabel(GUI_MSG_LABEL_SET, GetID(), GetID());
+
+ for (unsigned int i = 0; i < COUNTDOWN_DURATION_SEC; i++)
+ {
+ const unsigned int secondsElapsed = i;
+ const unsigned int secondsRemaining = COUNTDOWN_DURATION_SEC - i;
+
+ const bool bWarn = secondsElapsed >= WAIT_TO_WARN_SEC;
+
+ std::string strLabel;
+
+ if (bWarn)
+ strLabel = StringUtils::Format(strWarn, strFeature, secondsRemaining);
+ else
+ strLabel = StringUtils::Format(strPrompt, strFeature, secondsRemaining);
+
+ msgLabel.SetLabel(strLabel);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msgLabel, WINDOW_INVALID, false);
+
+ waitEvent.Reset();
+ bInterrupted = waitEvent.Wait(1000ms); // Wait 1 second
+
+ if (bInterrupted)
+ break;
+ }
+
+ // Reset label
+ msgLabel.SetLabel(m_feature.Label());
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msgLabel, WINDOW_INVALID, false);
+
+ return bInterrupted;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureButton.h b/xbmc/games/controllers/guicontrols/GUIFeatureButton.h
new file mode 100644
index 0000000..b69f521
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureButton.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016-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 "games/controllers/input/PhysicalFeature.h"
+#include "games/controllers/windows/IConfigurationWindow.h"
+#include "guilib/GUIButtonControl.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureButton : public CGUIButtonControl, public IFeatureButton
+{
+public:
+ CGUIFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIFeatureButton() override = default;
+
+ // implementation of CGUIControl via CGUIButtonControl
+ void OnUnFocus() override;
+
+ // partial implementation of IFeatureButton
+ const CPhysicalFeature& Feature() const override { return m_feature; }
+ INPUT::CARDINAL_DIRECTION GetCardinalDirection() const override
+ {
+ return INPUT::CARDINAL_DIRECTION::NONE;
+ }
+ JOYSTICK::WHEEL_DIRECTION GetWheelDirection() const override
+ {
+ return JOYSTICK::WHEEL_DIRECTION::NONE;
+ }
+ JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection() const override
+ {
+ return JOYSTICK::THROTTLE_DIRECTION::NONE;
+ }
+
+protected:
+ bool DoPrompt(const std::string& strPrompt,
+ const std::string& strWarn,
+ const std::string& strFeature,
+ CEvent& waitEvent);
+
+ // FSM helper
+ template<typename T>
+ T GetNextState(T state)
+ {
+ return static_cast<T>(static_cast<int>(state) + 1);
+ }
+
+ const CPhysicalFeature m_feature;
+
+private:
+ IConfigurationWizard* const m_wizard;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp
new file mode 100644
index 0000000..63d4757
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureControls.h"
+
+#include "games/controllers/windows/GUIControllerDefines.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIFeatureGroupTitle::CGUIFeatureGroupTitle(const CGUILabelControl& groupTitleTemplate,
+ const std::string& groupName,
+ unsigned int buttonIndex)
+ : CGUILabelControl(groupTitleTemplate)
+{
+ // Initialize CGUILabelControl
+ SetLabel(groupName);
+ SetID(CONTROL_FEATURE_GROUPS_START + buttonIndex);
+ SetVisible(true);
+ AllocResources();
+}
+
+CGUIFeatureSeparator::CGUIFeatureSeparator(const CGUIImage& separatorTemplate,
+ unsigned int buttonIndex)
+ : CGUIImage(separatorTemplate)
+{
+ // Initialize CGUIImage
+ SetID(CONTROL_FEATURE_SEPARATORS_START + buttonIndex);
+ SetVisible(true);
+ AllocResources();
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureControls.h b/xbmc/games/controllers/guicontrols/GUIFeatureControls.h
new file mode 100644
index 0000000..c72a11a
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureControls.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-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 "guilib/GUIImage.h"
+#include "guilib/GUILabelControl.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureGroupTitle : public CGUILabelControl
+{
+public:
+ CGUIFeatureGroupTitle(const CGUILabelControl& groupTitleTemplate,
+ const std::string& groupName,
+ unsigned int buttonIndex);
+
+ ~CGUIFeatureGroupTitle() override = default;
+};
+
+class CGUIFeatureSeparator : public CGUIImage
+{
+public:
+ CGUIFeatureSeparator(const CGUIImage& separatorTemplate, unsigned int buttonIndex);
+
+ ~CGUIFeatureSeparator() override = default;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp
new file mode 100644
index 0000000..2cce033
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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 "GUIFeatureFactory.h"
+
+#include "GUICardinalFeatureButton.h"
+#include "GUIScalarFeatureButton.h"
+#include "GUISelectKeyButton.h"
+#include "GUIThrottleButton.h"
+#include "GUIWheelButton.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIButtonControl* CGUIFeatureFactory::CreateButton(BUTTON_TYPE type,
+ const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+{
+ switch (type)
+ {
+ case BUTTON_TYPE::BUTTON:
+ return new CGUIScalarFeatureButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::ANALOG_STICK:
+ return new CGUIAnalogStickButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::WHEEL:
+ return new CGUIWheelButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::THROTTLE:
+ return new CGUIThrottleButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::SELECT_KEY:
+ return new CGUISelectKeyButton(buttonTemplate, wizard, index);
+
+ case BUTTON_TYPE::RELATIVE_POINTER:
+ return new CGUIRelativePointerButton(buttonTemplate, wizard, feature, index);
+
+ default:
+ break;
+ }
+
+ return nullptr;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h
new file mode 100644
index 0000000..e35070d
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "GUIControlTypes.h"
+
+class CGUIButtonControl;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalFeature;
+class IConfigurationWizard;
+
+class CGUIFeatureFactory
+{
+public:
+ /*!
+ * \brief Create a button of the specified type
+ * \param type The type of button control being created
+ * \return A button control, or nullptr if type is invalid
+ */
+ static CGUIButtonControl* CreateButton(BUTTON_TYPE type,
+ const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp
new file mode 100644
index 0000000..ab941c5
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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 "GUIFeatureTranslator.h"
+
+using namespace KODI;
+using namespace GAME;
+
+BUTTON_TYPE CGUIFeatureTranslator::GetButtonType(JOYSTICK::FEATURE_TYPE featureType)
+{
+ switch (featureType)
+ {
+ case JOYSTICK::FEATURE_TYPE::SCALAR:
+ case JOYSTICK::FEATURE_TYPE::KEY:
+ return BUTTON_TYPE::BUTTON;
+
+ case JOYSTICK::FEATURE_TYPE::ANALOG_STICK:
+ return BUTTON_TYPE::ANALOG_STICK;
+
+ case JOYSTICK::FEATURE_TYPE::RELPOINTER:
+ return BUTTON_TYPE::RELATIVE_POINTER;
+
+ case JOYSTICK::FEATURE_TYPE::WHEEL:
+ return BUTTON_TYPE::WHEEL;
+
+ case JOYSTICK::FEATURE_TYPE::THROTTLE:
+ return BUTTON_TYPE::THROTTLE;
+
+ default:
+ break;
+ }
+
+ return BUTTON_TYPE::UNKNOWN;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h
new file mode 100644
index 0000000..8e28d16
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "GUIControlTypes.h"
+#include "input/joysticks/JoystickTypes.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureTranslator
+{
+public:
+ /*!
+ * \brief Get the type of button control used to map the feature
+ */
+ static BUTTON_TYPE GetButtonType(JOYSTICK::FEATURE_TYPE featureType);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIGameController.cpp b/xbmc/games/controllers/guicontrols/GUIGameController.cpp
new file mode 100644
index 0000000..8990cf2
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIGameController.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014-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 "GUIGameController.h"
+
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIGameController::CGUIGameController(
+ int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIImage(parentID, controlID, posX, posY, width, height, CTextureInfo())
+{
+ // Initialize CGUIControl
+ ControlType = GUICONTROL_GAMECONTROLLER;
+}
+
+CGUIGameController::CGUIGameController(const CGUIGameController& from) : CGUIImage(from)
+{
+ // Initialize CGUIControl
+ ControlType = GUICONTROL_GAMECONTROLLER;
+}
+
+CGUIGameController* CGUIGameController::Clone(void) const
+{
+ return new CGUIGameController(*this);
+}
+
+void CGUIGameController::Render(void)
+{
+ CGUIImage::Render();
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (m_currentController)
+ {
+ //! @todo Render pressed buttons
+ }
+}
+
+void CGUIGameController::ActivateController(const ControllerPtr& controller)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (controller && controller != m_currentController)
+ {
+ m_currentController = controller;
+
+ lock.unlock();
+
+ //! @todo Sometimes this fails on window init
+ SetFileName(m_currentController->Layout().ImagePath());
+ }
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIGameController.h b/xbmc/games/controllers/guicontrols/GUIGameController.h
new file mode 100644
index 0000000..d62819a
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIGameController.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014-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 "games/controllers/ControllerTypes.h"
+#include "guilib/GUIImage.h"
+#include "threads/CriticalSection.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIGameController : public CGUIImage
+{
+public:
+ CGUIGameController(
+ int parentID, int controlID, float posX, float posY, float width, float height);
+ CGUIGameController(const CGUIGameController& from);
+
+ ~CGUIGameController() override = default;
+
+ // implementation of CGUIControl via CGUIImage
+ CGUIGameController* Clone() const override;
+ void Render() override;
+
+ void ActivateController(const ControllerPtr& controller);
+
+private:
+ ControllerPtr m_currentController;
+ CCriticalSection m_mutex;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp
new file mode 100644
index 0000000..5358aea
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016-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 "GUIScalarFeatureButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIScalarFeatureButton::CGUIScalarFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUIScalarFeatureButton::PromptForInput(CEvent& waitEvent)
+{
+ bool bInterrupted = false;
+
+ switch (m_state)
+ {
+ case STATE::NEED_INPUT:
+ {
+ const std::string& strPrompt = g_localizeStrings.Get(35090); // "Press %s"
+ const std::string& strWarn = g_localizeStrings.Get(35091); // "Press %s (%d)"
+
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ m_state = GetNextState(m_state);
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ return bInterrupted;
+}
+
+bool CGUIScalarFeatureButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+void CGUIScalarFeatureButton::Reset(void)
+{
+ m_state = STATE::NEED_INPUT;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h
new file mode 100644
index 0000000..6505684
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIScalarFeatureButton : public CGUIFeatureButton
+{
+public:
+ CGUIScalarFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIScalarFeatureButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ NEED_INPUT,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp
new file mode 100644
index 0000000..ea4a656
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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 "GUISelectKeyButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUISelectKeyButton::CGUISelectKeyButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, GetFeature(), index)
+{
+}
+
+const CPhysicalFeature& CGUISelectKeyButton::Feature(void) const
+{
+ if (m_state == STATE::NEED_INPUT)
+ return m_selectedKey;
+
+ return CGUIFeatureButton::Feature();
+}
+
+bool CGUISelectKeyButton::PromptForInput(CEvent& waitEvent)
+{
+ bool bInterrupted = false;
+
+ switch (m_state)
+ {
+ case STATE::NEED_KEY:
+ {
+ const std::string& strPrompt = g_localizeStrings.Get(35169); // "Press a key"
+ const std::string& strWarn = g_localizeStrings.Get(35170); // "Press a key ({1:d})"
+
+ bInterrupted = DoPrompt(strPrompt, strWarn, "", waitEvent);
+
+ m_state = GetNextState(m_state);
+
+ break;
+ }
+ case STATE::NEED_INPUT:
+ {
+ const std::string& strPrompt = g_localizeStrings.Get(35090); // "Press {0:s}"
+ const std::string& strWarn = g_localizeStrings.Get(35091); // "Press {0:s} ({1:d})"
+
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_selectedKey.Label(), waitEvent);
+
+ m_state = GetNextState(m_state);
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ return bInterrupted;
+}
+
+bool CGUISelectKeyButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+void CGUISelectKeyButton::SetKey(const CPhysicalFeature& key)
+{
+ m_selectedKey = key;
+}
+
+void CGUISelectKeyButton::Reset(void)
+{
+ m_state = STATE::NEED_KEY;
+ m_selectedKey.Reset();
+}
+
+CPhysicalFeature CGUISelectKeyButton::GetFeature()
+{
+ return CPhysicalFeature(35168); // "Select key"
+}
diff --git a/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h
new file mode 100644
index 0000000..e96f78b
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "GUIFeatureButton.h"
+#include "games/controllers/input/PhysicalFeature.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUISelectKeyButton : public CGUIFeatureButton
+{
+public:
+ CGUISelectKeyButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ unsigned int index);
+
+ ~CGUISelectKeyButton() override = default;
+
+ // implementation of IFeatureButton
+ const CPhysicalFeature& Feature(void) const override;
+ bool AllowWizard() const override { return false; }
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ bool NeedsKey() const override { return m_state == STATE::NEED_KEY; }
+ void SetKey(const CPhysicalFeature& key) override;
+ void Reset() override;
+
+private:
+ static CPhysicalFeature GetFeature();
+
+ enum class STATE
+ {
+ NEED_KEY,
+ NEED_INPUT,
+ FINISHED,
+ };
+
+ STATE m_state = STATE::NEED_KEY;
+
+ CPhysicalFeature m_selectedKey;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp b/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp
new file mode 100644
index 0000000..69f32a1
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016-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 "GUIThrottleButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIThrottleButton::CGUIThrottleButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUIThrottleButton::PromptForInput(CEvent& waitEvent)
+{
+ using namespace JOYSTICK;
+
+ bool bInterrupted = false;
+
+ // Get the prompt for the current analog stick direction
+ std::string strPrompt;
+ std::string strWarn;
+ switch (m_state)
+ {
+ case STATE::THROTTLE_UP:
+ strPrompt = g_localizeStrings.Get(35092); // "Move %s up"
+ strWarn = g_localizeStrings.Get(35093); // "Move %s up (%d)"
+ break;
+ case STATE::THROTTLE_DOWN:
+ strPrompt = g_localizeStrings.Get(35094); // "Move %s down"
+ strWarn = g_localizeStrings.Get(35095); // "Move %s down (%d)"
+ break;
+ default:
+ break;
+ }
+
+ if (!strPrompt.empty())
+ {
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ if (!bInterrupted)
+ m_state = STATE::FINISHED; // Not interrupted, must have timed out
+ else
+ m_state = GetNextState(m_state); // Interrupted by input, proceed
+ }
+
+ return bInterrupted;
+}
+
+bool CGUIThrottleButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+JOYSTICK::THROTTLE_DIRECTION CGUIThrottleButton::GetThrottleDirection(void) const
+{
+ using namespace JOYSTICK;
+
+ switch (m_state)
+ {
+ case STATE::THROTTLE_UP:
+ return THROTTLE_DIRECTION::UP;
+ case STATE::THROTTLE_DOWN:
+ return THROTTLE_DIRECTION::DOWN;
+ default:
+ break;
+ }
+
+ return THROTTLE_DIRECTION::NONE;
+}
+
+void CGUIThrottleButton::Reset(void)
+{
+ m_state = STATE::THROTTLE_UP;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIThrottleButton.h b/xbmc/games/controllers/guicontrols/GUIThrottleButton.h
new file mode 100644
index 0000000..72b7ee3
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIThrottleButton.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIThrottleButton : public CGUIFeatureButton
+{
+public:
+ CGUIThrottleButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIThrottleButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ THROTTLE_UP,
+ THROTTLE_DOWN,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp b/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp
new file mode 100644
index 0000000..63c3819
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016-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 "GUIWheelButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIWheelButton::CGUIWheelButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUIWheelButton::PromptForInput(CEvent& waitEvent)
+{
+ using namespace JOYSTICK;
+
+ bool bInterrupted = false;
+
+ // Get the prompt for the current analog stick direction
+ std::string strPrompt;
+ std::string strWarn;
+ switch (m_state)
+ {
+ case STATE::WHEEL_LEFT:
+ strPrompt = g_localizeStrings.Get(35098); // "Move %s left"
+ strWarn = g_localizeStrings.Get(35099); // "Move %s left (%d)"
+ break;
+ case STATE::WHEEL_RIGHT:
+ strPrompt = g_localizeStrings.Get(35096); // "Move %s right"
+ strWarn = g_localizeStrings.Get(35097); // "Move %s right (%d)"
+ break;
+ default:
+ break;
+ }
+
+ if (!strPrompt.empty())
+ {
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ if (!bInterrupted)
+ m_state = STATE::FINISHED; // Not interrupted, must have timed out
+ else
+ m_state = GetNextState(m_state); // Interrupted by input, proceed
+ }
+
+ return bInterrupted;
+}
+
+bool CGUIWheelButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+JOYSTICK::WHEEL_DIRECTION CGUIWheelButton::GetWheelDirection(void) const
+{
+ using namespace JOYSTICK;
+
+ switch (m_state)
+ {
+ case STATE::WHEEL_LEFT:
+ return WHEEL_DIRECTION::LEFT;
+ case STATE::WHEEL_RIGHT:
+ return WHEEL_DIRECTION::RIGHT;
+ default:
+ break;
+ }
+
+ return WHEEL_DIRECTION::NONE;
+}
+
+void CGUIWheelButton::Reset(void)
+{
+ m_state = STATE::WHEEL_LEFT;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIWheelButton.h b/xbmc/games/controllers/guicontrols/GUIWheelButton.h
new file mode 100644
index 0000000..937703a
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIWheelButton.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIWheelButton : public CGUIFeatureButton
+{
+public:
+ CGUIWheelButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIWheelButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ JOYSTICK::WHEEL_DIRECTION GetWheelDirection() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ WHEEL_LEFT,
+ WHEEL_RIGHT,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/input/CMakeLists.txt b/xbmc/games/controllers/input/CMakeLists.txt
new file mode 100644
index 0000000..511fa37
--- /dev/null
+++ b/xbmc/games/controllers/input/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES InputSink.cpp
+ PhysicalFeature.cpp
+ PhysicalTopology.cpp
+)
+
+set(HEADERS InputSink.h
+ PhysicalFeature.h
+ PhysicalTopology.h
+)
+
+core_add_library(games_controller_input)
diff --git a/xbmc/games/controllers/input/InputSink.cpp b/xbmc/games/controllers/input/InputSink.cpp
new file mode 100644
index 0000000..a48b4b9
--- /dev/null
+++ b/xbmc/games/controllers/input/InputSink.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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 "InputSink.h"
+
+#include "games/controllers/ControllerIDs.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CInputSink::CInputSink(JOYSTICK::IInputHandler* gameInput) : m_gameInput(gameInput)
+{
+}
+
+std::string CInputSink::ControllerID(void) const
+{
+ return DEFAULT_CONTROLLER_ID;
+}
+
+bool CInputSink::AcceptsInput(const std::string& feature) const
+{
+ return m_gameInput->AcceptsInput(feature);
+}
+
+bool CInputSink::OnButtonPress(const std::string& feature, bool bPressed)
+{
+ return true;
+}
+
+bool CInputSink::OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
+
+bool CInputSink::OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
+
+bool CInputSink::OnAccelerometerMotion(const std::string& feature, float x, float y, float z)
+{
+ return true;
+}
+
+bool CInputSink::OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
+
+bool CInputSink::OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
diff --git a/xbmc/games/controllers/input/InputSink.h b/xbmc/games/controllers/input/InputSink.h
new file mode 100644
index 0000000..5dc331f
--- /dev/null
+++ b/xbmc/games/controllers/input/InputSink.h
@@ -0,0 +1,53 @@
+/*
+ * 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/joysticks/interfaces/IInputHandler.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+
+class CInputSink : public JOYSTICK::IInputHandler
+{
+public:
+ explicit CInputSink(JOYSTICK::IInputHandler* gameInput);
+
+ ~CInputSink() override = default;
+
+ // 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 {}
+
+private:
+ // Construction parameters
+ JOYSTICK::IInputHandler* m_gameInput;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/input/PhysicalFeature.cpp b/xbmc/games/controllers/input/PhysicalFeature.cpp
new file mode 100644
index 0000000..24e0c41
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalFeature.cpp
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015-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 "PhysicalFeature.h"
+
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerDefinitions.h"
+#include "games/controllers/ControllerTranslator.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+
+using namespace KODI;
+using namespace GAME;
+using namespace JOYSTICK;
+
+CPhysicalFeature::CPhysicalFeature(int labelId)
+{
+ Reset();
+ m_labelId = labelId;
+}
+
+void CPhysicalFeature::Reset(void)
+{
+ *this = CPhysicalFeature();
+}
+
+CPhysicalFeature& CPhysicalFeature::operator=(const CPhysicalFeature& rhs)
+{
+ if (this != &rhs)
+ {
+ m_controller = rhs.m_controller;
+ m_type = rhs.m_type;
+ m_category = rhs.m_category;
+ m_categoryLabelId = rhs.m_categoryLabelId;
+ m_strName = rhs.m_strName;
+ m_labelId = rhs.m_labelId;
+ m_inputType = rhs.m_inputType;
+ m_keycode = rhs.m_keycode;
+ }
+ return *this;
+}
+
+std::string CPhysicalFeature::CategoryLabel() const
+{
+ std::string categoryLabel;
+
+ if (m_categoryLabelId >= 0 && m_controller != nullptr)
+ categoryLabel = g_localizeStrings.GetAddonString(m_controller->ID(), m_categoryLabelId);
+
+ if (categoryLabel.empty())
+ categoryLabel = g_localizeStrings.Get(m_categoryLabelId);
+
+ return categoryLabel;
+}
+
+std::string CPhysicalFeature::Label() const
+{
+ std::string label;
+
+ if (m_labelId >= 0 && m_controller != nullptr)
+ label = g_localizeStrings.GetAddonString(m_controller->ID(), m_labelId);
+
+ if (label.empty())
+ label = g_localizeStrings.Get(m_labelId);
+
+ return label;
+}
+
+bool CPhysicalFeature::Deserialize(const TiXmlElement* pElement,
+ const CController* controller,
+ FEATURE_CATEGORY category,
+ int categoryLabelId)
+{
+ Reset();
+
+ if (!pElement)
+ return false;
+
+ std::string strType(pElement->Value());
+
+ // Type
+ m_type = CControllerTranslator::TranslateFeatureType(strType);
+ if (m_type == FEATURE_TYPE::UNKNOWN)
+ {
+ CLog::Log(LOGDEBUG, "Invalid feature: <{}> ", pElement->Value());
+ return false;
+ }
+
+ // Cagegory was obtained from parent XML node
+ m_category = category;
+ m_categoryLabelId = categoryLabelId;
+
+ // Name
+ m_strName = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_FEATURE_NAME);
+ if (m_strName.empty())
+ {
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_FEATURE_NAME);
+ return false;
+ }
+
+ // Label ID
+ std::string strLabel = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_FEATURE_LABEL);
+ if (strLabel.empty())
+ CLog::Log(LOGDEBUG, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_FEATURE_LABEL);
+ else
+ std::istringstream(strLabel) >> m_labelId;
+
+ // Input type
+ if (m_type == FEATURE_TYPE::SCALAR)
+ {
+ std::string strInputType = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_INPUT_TYPE);
+ if (strInputType.empty())
+ {
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_INPUT_TYPE);
+ return false;
+ }
+ else
+ {
+ m_inputType = CControllerTranslator::TranslateInputType(strInputType);
+ if (m_inputType == INPUT_TYPE::UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "<{}> tag - attribute \"{}\" is invalid: \"{}\"", strType,
+ LAYOUT_XML_ATTR_INPUT_TYPE, strInputType);
+ return false;
+ }
+ }
+ }
+
+ // Keycode
+ if (m_type == FEATURE_TYPE::KEY)
+ {
+ std::string strSymbol = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_KEY_SYMBOL);
+ if (strSymbol.empty())
+ {
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_KEY_SYMBOL);
+ return false;
+ }
+ else
+ {
+ m_keycode = CControllerTranslator::TranslateKeysym(strSymbol);
+ if (m_keycode == XBMCK_UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "<{}> tag - attribute \"{}\" is invalid: \"{}\"", strType,
+ LAYOUT_XML_ATTR_KEY_SYMBOL, strSymbol);
+ return false;
+ }
+ }
+ }
+
+ // Save controller for string translation
+ m_controller = controller;
+
+ return true;
+}
diff --git a/xbmc/games/controllers/input/PhysicalFeature.h b/xbmc/games/controllers/input/PhysicalFeature.h
new file mode 100644
index 0000000..6319a63
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalFeature.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015-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 "games/controllers/ControllerTypes.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/keyboard/KeyboardTypes.h"
+
+#include <string>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CPhysicalFeature
+{
+public:
+ CPhysicalFeature() = default;
+ CPhysicalFeature(int labelId);
+ CPhysicalFeature(const CPhysicalFeature& other) { *this = other; }
+
+ void Reset(void);
+
+ CPhysicalFeature& operator=(const CPhysicalFeature& rhs);
+
+ JOYSTICK::FEATURE_TYPE Type(void) const { return m_type; }
+ JOYSTICK::FEATURE_CATEGORY Category(void) const { return m_category; }
+ const std::string& Name(void) const { return m_strName; }
+
+ // GUI properties
+ std::string Label(void) const;
+ int LabelID(void) const { return m_labelId; }
+ std::string CategoryLabel(void) const;
+
+ // Input properties
+ JOYSTICK::INPUT_TYPE InputType(void) const { return m_inputType; }
+ KEYBOARD::KeySymbol Keycode() const { return m_keycode; }
+
+ bool Deserialize(const TiXmlElement* pElement,
+ const CController* controller,
+ JOYSTICK::FEATURE_CATEGORY category,
+ int categoryLabelId);
+
+private:
+ const CController* m_controller = nullptr; // Used for translating addon-specific labels
+ JOYSTICK::FEATURE_TYPE m_type = JOYSTICK::FEATURE_TYPE::UNKNOWN;
+ JOYSTICK::FEATURE_CATEGORY m_category = JOYSTICK::FEATURE_CATEGORY::UNKNOWN;
+ int m_categoryLabelId = -1;
+ std::string m_strName;
+ int m_labelId = -1;
+ JOYSTICK::INPUT_TYPE m_inputType = JOYSTICK::INPUT_TYPE::UNKNOWN;
+ KEYBOARD::KeySymbol m_keycode = XBMCK_UNKNOWN;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/input/PhysicalTopology.cpp b/xbmc/games/controllers/input/PhysicalTopology.cpp
new file mode 100644
index 0000000..2fecb5a
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalTopology.cpp
@@ -0,0 +1,56 @@
+/*
+ * 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 "PhysicalTopology.h"
+
+#include "games/controllers/ControllerDefinitions.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CPhysicalTopology::CPhysicalTopology(bool bProvidesInput, std::vector<CPhysicalPort> ports)
+ : m_bProvidesInput(bProvidesInput), m_ports(std::move(ports))
+{
+}
+
+void CPhysicalTopology::Reset()
+{
+ CPhysicalTopology defaultTopology;
+ *this = std::move(defaultTopology);
+}
+
+bool CPhysicalTopology::Deserialize(const TiXmlElement* pElement)
+{
+ Reset();
+
+ if (pElement == nullptr)
+ return false;
+
+ m_bProvidesInput = (XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_PROVIDES_INPUT) != "false");
+
+ for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr;
+ pChild = pChild->NextSiblingElement())
+ {
+ if (pChild->ValueStr() == LAYOUT_XML_ELM_PORT)
+ {
+ CPhysicalPort port;
+ if (port.Deserialize(pChild))
+ m_ports.emplace_back(std::move(port));
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Unknown physical topology tag: <{}>", pChild->ValueStr());
+ }
+ }
+
+ return true;
+}
diff --git a/xbmc/games/controllers/input/PhysicalTopology.h b/xbmc/games/controllers/input/PhysicalTopology.h
new file mode 100644
index 0000000..c194de8
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalTopology.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "games/ports/input/PhysicalPort.h"
+
+#include <vector>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+
+/*!
+ * \brief Represents the physical topology of controller add-ons
+ *
+ * The physical topology of a controller defines how many ports it has and
+ * whether it can provide player input (hubs like the Super Multitap don't
+ * provide input).
+ */
+class CPhysicalTopology
+{
+public:
+ CPhysicalTopology() = default;
+ CPhysicalTopology(bool bProvidesInput, std::vector<CPhysicalPort> ports);
+
+ void Reset();
+
+ /*!
+ * \brief Check if the controller can provide player input
+ *
+ * This allows hubs to specify that they provide no input
+ *
+ * \return True if the controller can provide player input, false otherwise
+ */
+ bool ProvidesInput() const { return m_bProvidesInput; }
+
+ /*!
+ * \brief Get a list of ports provided by this controller
+ *
+ * \return The ports
+ */
+ const std::vector<CPhysicalPort>& Ports() const { return m_ports; }
+
+ bool Deserialize(const TiXmlElement* pElement);
+
+private:
+ bool m_bProvidesInput = true;
+ std::vector<CPhysicalPort> m_ports;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/CMakeLists.txt b/xbmc/games/controllers/types/CMakeLists.txt
new file mode 100644
index 0000000..503d254
--- /dev/null
+++ b/xbmc/games/controllers/types/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES ControllerGrid.cpp
+ ControllerHub.cpp
+ ControllerNode.cpp
+)
+
+set(HEADERS ControllerGrid.h
+ ControllerHub.h
+ ControllerNode.h
+ ControllerTree.h
+)
+
+core_add_library(games_controller_types)
diff --git a/xbmc/games/controllers/types/ControllerGrid.cpp b/xbmc/games/controllers/types/ControllerGrid.cpp
new file mode 100644
index 0000000..f718d5a
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerGrid.cpp
@@ -0,0 +1,242 @@
+/*
+ * 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 "ControllerGrid.h"
+
+#include "games/controllers/Controller.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerGrid::~CControllerGrid() = default;
+
+void CControllerGrid::SetControllerTree(const CControllerTree& controllerTree)
+{
+ // Clear the result
+ m_grid.clear();
+
+ m_height = AddPorts(controllerTree.GetPorts(), m_grid);
+ SetHeight(m_height, m_grid);
+}
+
+ControllerVector CControllerGrid::GetControllers(unsigned int playerIndex) const
+{
+ ControllerVector controllers;
+
+ if (playerIndex < m_grid.size())
+ {
+ for (const auto& controllerVertex : m_grid[playerIndex].vertices)
+ {
+ if (controllerVertex.controller)
+ controllers.emplace_back(controllerVertex.controller);
+ }
+ }
+
+ return controllers;
+}
+
+unsigned int CControllerGrid::AddPorts(const PortVec& ports, ControllerGrid& grid)
+{
+ unsigned int height = 0;
+
+ auto itKeyboard = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) {
+ return port.GetPortType() == PORT_TYPE::KEYBOARD;
+ });
+
+ auto itMouse = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) {
+ return port.GetPortType() == PORT_TYPE::MOUSE;
+ });
+
+ auto itController = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) {
+ return port.GetPortType() == PORT_TYPE::CONTROLLER;
+ });
+
+ // Keyboard and mouse are not allowed to have ports because they might
+ // overlap with controllers
+ if (itKeyboard != ports.end() && itKeyboard->GetActiveController().GetHub().HasPorts())
+ {
+ CLog::Log(LOGERROR, "Found keyboard with controller ports, skipping");
+ itKeyboard = ports.end();
+ }
+ if (itMouse != ports.end() && itMouse->GetActiveController().GetHub().HasPorts())
+ {
+ CLog::Log(LOGERROR, "Found mouse with controller ports, skipping");
+ itMouse = ports.end();
+ }
+
+ if (itController != ports.end())
+ {
+ // Add controller ports
+ bool bFirstPlayer = true;
+ for (const CPortNode& port : ports)
+ {
+ ControllerColumn column;
+
+ if (port.GetPortType() == PORT_TYPE::CONTROLLER)
+ {
+ // Add controller
+ height =
+ std::max(height, AddController(port, static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+
+ if (bFirstPlayer)
+ {
+ bFirstPlayer = false;
+
+ // Keyboard and mouse are added below the first controller
+ if (itKeyboard != ports.end())
+ height =
+ std::max(height, AddController(*itKeyboard,
+ static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+ if (itMouse != ports.end())
+ height = std::max(
+ height, AddController(*itMouse, static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+ }
+ }
+
+ if (!column.vertices.empty())
+ grid.emplace_back(std::move(column));
+ }
+ }
+ else
+ {
+ // No controllers, add keyboard and mouse
+ ControllerColumn column;
+
+ if (itKeyboard != ports.end())
+ height = std::max(height, AddController(*itKeyboard,
+ static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+ if (itMouse != ports.end())
+ height = std::max(height,
+ AddController(*itMouse, static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+
+ if (!column.vertices.empty())
+ grid.emplace_back(std::move(column));
+ }
+
+ return height;
+}
+
+unsigned int CControllerGrid::AddController(const CPortNode& port,
+ unsigned int height,
+ std::vector<ControllerVertex>& column,
+ ControllerGrid& grid)
+{
+ // Add spacers
+ while (column.size() < height)
+ AddInvisible(column);
+
+ const CControllerNode& activeController = port.GetActiveController();
+
+ // Add vertex
+ ControllerVertex vertex;
+ vertex.bVisible = true;
+ vertex.bConnected = port.IsConnected();
+ vertex.portType = port.GetPortType();
+ vertex.controller = activeController.GetController();
+ vertex.address = activeController.GetControllerAddress();
+ for (const CControllerNode& node : port.GetCompatibleControllers())
+ vertex.compatible.emplace_back(node.GetController());
+ column.emplace_back(std::move(vertex));
+
+ height++;
+
+ // Process ports
+ const PortVec& ports = activeController.GetHub().GetPorts();
+ if (!ports.empty())
+ {
+ switch (GetDirection(activeController))
+ {
+ case GRID_DIRECTION::RIGHT:
+ {
+ height = std::max(height, AddHub(ports, height - 1, false, grid));
+ break;
+ }
+ case GRID_DIRECTION::DOWN:
+ {
+ const unsigned int row = height;
+
+ // Add the first controller to the column
+ const CPortNode firstController = ports.at(0);
+ height = std::max(height, AddController(firstController, row, column, grid));
+
+ // Add the remaining controllers on the same row
+ height = std::max(height, AddHub(ports, row, true, grid));
+
+ break;
+ }
+ }
+ }
+
+ return height;
+}
+
+unsigned int CControllerGrid::AddHub(const PortVec& ports,
+ unsigned int height,
+ bool bSkipFirst,
+ ControllerGrid& grid)
+{
+ const unsigned int row = height;
+
+ unsigned int port = 0;
+ for (const auto& controllerPort : ports)
+ {
+ // If controller has no player, it has already added the hub's first controller
+ if (bSkipFirst && port == 0)
+ continue;
+
+ // Add a column for this controller
+ grid.emplace_back();
+ ControllerColumn& column = grid.back();
+
+ height = std::max(height, AddController(controllerPort, row, column.vertices, grid));
+
+ port++;
+ }
+
+ return height;
+}
+
+void CControllerGrid::AddInvisible(std::vector<ControllerVertex>& column)
+{
+ ControllerVertex vertex;
+ vertex.bVisible = false;
+ column.emplace_back(std::move(vertex));
+}
+
+void CControllerGrid::SetHeight(unsigned int height, ControllerGrid& grid)
+{
+ for (auto& column : grid)
+ {
+ while (static_cast<unsigned int>(column.vertices.size()) < height)
+ AddInvisible(column.vertices);
+ }
+}
+
+CControllerGrid::GRID_DIRECTION CControllerGrid::GetDirection(const CControllerNode& node)
+{
+ // Hub controllers are added horizontally, one per row.
+ //
+ // If the current controller offers a player spot, the row starts to the
+ // right at the same height as the controller.
+ //
+ // Otherwise, to row starts below the current controller in the same
+ // column.
+ if (node.ProvidesInput())
+ return GRID_DIRECTION::RIGHT;
+ else
+ return GRID_DIRECTION::DOWN;
+}
diff --git a/xbmc/games/controllers/types/ControllerGrid.h b/xbmc/games/controllers/types/ControllerGrid.h
new file mode 100644
index 0000000..44ec1d3
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerGrid.h
@@ -0,0 +1,167 @@
+/*
+ * 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 "ControllerTree.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Vertex in the grid of controllers
+ */
+struct ControllerVertex
+{
+ bool bVisible = true;
+ bool bConnected = false;
+ ControllerPtr controller; // Mandatory if connected
+ PORT_TYPE portType = PORT_TYPE::UNKNOWN; // Optional
+ std::string address; // Optional
+ ControllerVector compatible; // Compatible controllers
+};
+
+/*!
+ * \brief Column of controllers in the grid
+ */
+struct ControllerColumn
+{
+ std::vector<ControllerVertex> vertices;
+};
+
+/*!
+ * \brief Collection of controllers in a grid layout
+ */
+using ControllerGrid = std::vector<ControllerColumn>;
+
+/*!
+ * \brief Class to encapsulate grid operations
+ */
+class CControllerGrid
+{
+public:
+ CControllerGrid() = default;
+ CControllerGrid(const CControllerGrid& other) = default;
+ ~CControllerGrid();
+
+ /*!
+ * \brief Create a grid from a controller tree
+ */
+ void SetControllerTree(const CControllerTree& controllerTree);
+
+ /*!
+ * \brief Get the width of the controller grid
+ */
+ unsigned int GetWidth() const { return static_cast<unsigned int>(m_grid.size()); }
+
+ /*!
+ * \brief Get the height (deepest controller) of the controller grid
+ *
+ * The height is cached when the controller grid is created to avoid
+ * iterating the grid
+ */
+ unsigned int GetHeight() const { return m_height; }
+
+ /*!
+ * \brief Access the controller grid
+ */
+ const ControllerGrid& GetGrid() const { return m_grid; }
+
+ /*!
+ * \brief Get the controllers in use by the specified player
+ *
+ * \param playerIndex The column in the grid to get controllers from
+ */
+ ControllerVector GetControllers(unsigned int playerIndex) const;
+
+private:
+ /*!
+ * \brief Directions of vertex traversal
+ */
+ enum class GRID_DIRECTION
+ {
+ RIGHT,
+ DOWN,
+ };
+
+ /*!
+ * \brief Add ports to the grid
+ *
+ * \param ports The ports on a console or controller
+ * \param[out] grid The controller grid being created
+ *
+ * \return The height of the grid determined by the maximum column height
+ */
+ static unsigned int AddPorts(const PortVec& ports, ControllerGrid& grid);
+
+ /*!
+ * \brief Draw a controller to the column at the specified height
+ *
+ * \param port The controller's port node
+ * \param height The height to draw the controller at
+ * \param column[in/out] The column to draw to
+ * \param grid[in/out] The grid to add additional columns to
+ *
+ * \return The height of the grid
+ */
+ static unsigned int AddController(const CPortNode& port,
+ unsigned int height,
+ std::vector<ControllerVertex>& column,
+ ControllerGrid& grid);
+
+ /*!
+ * \brief Draw a series of controllers to the grid at the specified height
+ *
+ * \param ports The ports of the controllers to draw
+ * \param height The height to start drawing the controllers at
+ * \param bSkipFirst True if the first controller has already been drawn to
+ * a column, false to start drawing at the first controller
+ * \param grid[in/out] The grid to add columns to
+ *
+ * \return The height of the grid
+ */
+ static unsigned int AddHub(const PortVec& ports,
+ unsigned int height,
+ bool bSkipFirst,
+ ControllerGrid& grid);
+
+ /*!
+ * \brief Draw an invisible vertex to the column
+ *
+ * \param[in/out] column The column in a controller grid
+ */
+ static void AddInvisible(std::vector<ControllerVertex>& column);
+
+ /*!
+ * \brief Fill all columns with invisible vertices until the specified height
+ *
+ * \param height The height to make all columns
+ * \param[in/out] grid The grid to update
+ */
+ static void SetHeight(unsigned int height, ControllerGrid& grid);
+
+ /*!
+ * \brief Get the direction of traversal for the next vertex
+ *
+ * \param node The node in the controller tree being visited
+ *
+ * \return The direction of the next vertex, or GRID_DIRECTION::UNKNOWN if
+ * unknown
+ */
+ static GRID_DIRECTION GetDirection(const CControllerNode& node);
+
+ ControllerGrid m_grid;
+ unsigned int m_height = 0;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/ControllerHub.cpp b/xbmc/games/controllers/types/ControllerHub.cpp
new file mode 100644
index 0000000..5fd0ece
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerHub.cpp
@@ -0,0 +1,109 @@
+/*
+ * 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 "ControllerHub.h"
+
+#include "ControllerTree.h"
+#include "games/controllers/Controller.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerHub::~CControllerHub() = default;
+
+CControllerHub& CControllerHub::operator=(const CControllerHub& rhs)
+{
+ if (this != &rhs)
+ {
+ m_ports = rhs.m_ports;
+ }
+
+ return *this;
+}
+
+CControllerHub& CControllerHub::operator=(CControllerHub&& rhs) noexcept
+{
+ if (this != &rhs)
+ {
+ m_ports = std::move(rhs.m_ports);
+ }
+
+ return *this;
+}
+
+void CControllerHub::Clear()
+{
+ m_ports.clear();
+}
+
+void CControllerHub::SetPorts(PortVec ports)
+{
+ m_ports = std::move(ports);
+}
+
+bool CControllerHub::IsControllerAccepted(const std::string& controllerId) const
+{
+ return std::any_of(m_ports.begin(), m_ports.end(), [controllerId](const CPortNode& port) {
+ return port.IsControllerAccepted(controllerId);
+ });
+}
+
+bool CControllerHub::IsControllerAccepted(const std::string& portAddress,
+ const std::string& controllerId) const
+{
+ return std::any_of(m_ports.begin(), m_ports.end(),
+ [portAddress, controllerId](const CPortNode& port) {
+ return port.IsControllerAccepted(portAddress, controllerId);
+ });
+}
+
+ControllerVector CControllerHub::GetControllers() const
+{
+ ControllerVector controllers;
+ GetControllers(controllers);
+ return controllers;
+}
+
+void CControllerHub::GetControllers(ControllerVector& controllers) const
+{
+ for (const CPortNode& port : m_ports)
+ {
+ for (const CControllerNode& node : port.GetCompatibleControllers())
+ node.GetControllers(controllers);
+ }
+}
+
+const CPortNode& CControllerHub::GetPort(const std::string& address) const
+{
+ return GetPortInternal(m_ports, address);
+}
+
+const CPortNode& CControllerHub::GetPortInternal(const PortVec& ports, const std::string& address)
+{
+ for (const CPortNode& port : ports)
+ {
+ // Base case
+ if (port.GetAddress() == address)
+ return port;
+
+ // Check children
+ for (const CControllerNode& controller : port.GetCompatibleControllers())
+ {
+ const CPortNode& port = GetPortInternal(controller.GetHub().GetPorts(), address);
+ if (port.GetAddress() == address)
+ return port;
+ }
+ }
+
+ // Not found
+ static const CPortNode empty{};
+ return empty;
+}
diff --git a/xbmc/games/controllers/types/ControllerHub.h b/xbmc/games/controllers/types/ControllerHub.h
new file mode 100644
index 0000000..fc48a81
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerHub.h
@@ -0,0 +1,53 @@
+/*
+ * 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/ports/types/PortNode.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief A branch in the controller tree
+ */
+class CControllerHub
+{
+public:
+ CControllerHub() = default;
+ CControllerHub(const CControllerHub& other) { *this = other; }
+ CControllerHub(CControllerHub&& other) = default;
+ CControllerHub& operator=(const CControllerHub& rhs);
+ CControllerHub& operator=(CControllerHub&& rhs) noexcept;
+ ~CControllerHub();
+
+ void Clear();
+
+ bool HasPorts() const { return !m_ports.empty(); }
+ PortVec& GetPorts() { return m_ports; }
+ const PortVec& GetPorts() const { return m_ports; }
+ void SetPorts(PortVec ports);
+
+ bool IsControllerAccepted(const std::string& controllerId) const;
+ bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const;
+ ControllerVector GetControllers() const;
+ void GetControllers(ControllerVector& controllers) const;
+
+ const CPortNode& GetPort(const std::string& address) const;
+
+private:
+ static const CPortNode& GetPortInternal(const PortVec& ports, const std::string& address);
+
+ PortVec m_ports;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/ControllerNode.cpp b/xbmc/games/controllers/types/ControllerNode.cpp
new file mode 100644
index 0000000..4bfda73
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerNode.cpp
@@ -0,0 +1,136 @@
+/*
+ * 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 "ControllerNode.h"
+
+#include "ControllerHub.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "games/ports/types/PortNode.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerNode::CControllerNode() : m_hub(new CControllerHub)
+{
+}
+
+CControllerNode::~CControllerNode() = default;
+
+CControllerNode& CControllerNode::operator=(const CControllerNode& rhs)
+{
+ if (this != &rhs)
+ {
+ m_controller = rhs.m_controller;
+ m_portAddress = rhs.m_portAddress;
+ m_controllerAddress = rhs.m_controllerAddress;
+ m_hub.reset(new CControllerHub(*rhs.m_hub));
+ }
+
+ return *this;
+}
+
+CControllerNode& CControllerNode::operator=(CControllerNode&& rhs) noexcept
+{
+ if (this != &rhs)
+ {
+ m_controller = std::move(rhs.m_controller);
+ m_portAddress = std::move(rhs.m_portAddress);
+ m_controllerAddress = std::move(rhs.m_controllerAddress);
+ m_hub = std::move(rhs.m_hub);
+ }
+
+ return *this;
+}
+
+void CControllerNode::Clear()
+{
+ m_controller.reset();
+ m_portAddress.clear();
+ m_controllerAddress.clear();
+ m_hub.reset(new CControllerHub);
+}
+
+void CControllerNode::SetController(ControllerPtr controller)
+{
+ m_controller = std::move(controller);
+}
+
+void CControllerNode::GetControllers(ControllerVector& controllers) const
+{
+ if (m_controller)
+ {
+ const ControllerPtr& myController = m_controller;
+
+ auto it = std::find_if(controllers.begin(), controllers.end(),
+ [&myController](const ControllerPtr& controller) {
+ return myController->ID() == controller->ID();
+ });
+
+ if (it == controllers.end())
+ controllers.emplace_back(m_controller);
+ }
+
+ m_hub->GetControllers(controllers);
+}
+
+void CControllerNode::SetPortAddress(std::string portAddress)
+{
+ m_portAddress = std::move(portAddress);
+}
+
+void CControllerNode::SetControllerAddress(std::string controllerAddress)
+{
+ m_controllerAddress = std::move(controllerAddress);
+}
+
+void CControllerNode::SetHub(CControllerHub hub)
+{
+ m_hub.reset(new CControllerHub(std::move(hub)));
+}
+
+bool CControllerNode::IsControllerAccepted(const std::string& controllerId) const
+{
+ bool bAccepted = false;
+
+ for (const auto& port : m_hub->GetPorts())
+ {
+ if (port.IsControllerAccepted(controllerId))
+ {
+ bAccepted = true;
+ break;
+ }
+ }
+
+ return bAccepted;
+}
+
+bool CControllerNode::IsControllerAccepted(const std::string& portAddress,
+ const std::string& controllerId) const
+{
+ bool bAccepted = false;
+
+ for (const auto& port : m_hub->GetPorts())
+ {
+ if (port.IsControllerAccepted(portAddress, controllerId))
+ {
+ bAccepted = true;
+ break;
+ }
+ }
+
+ return bAccepted;
+}
+
+bool CControllerNode::ProvidesInput() const
+{
+ return m_controller && m_controller->Topology().ProvidesInput();
+}
diff --git a/xbmc/games/controllers/types/ControllerNode.h b/xbmc/games/controllers/types/ControllerNode.h
new file mode 100644
index 0000000..d4d86e5
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerNode.h
@@ -0,0 +1,118 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerHub;
+
+/*!
+ * \brief Node in the controller tree
+ *
+ * The node identifies the controller profile, and optionally the available
+ * controller ports.
+ */
+class CControllerNode
+{
+public:
+ CControllerNode();
+ CControllerNode(const CControllerNode& other) { *this = other; }
+ CControllerNode(CControllerNode&& other) = default;
+ CControllerNode& operator=(const CControllerNode& rhs);
+ CControllerNode& operator=(CControllerNode&& rhs) noexcept;
+ ~CControllerNode();
+
+ void Clear();
+
+ /*!
+ * \brief Controller profile of this code
+ *
+ * \return Controller profile, or empty if this node is invalid
+ *
+ * \sa IsValid()
+ */
+ const ControllerPtr& GetController() const { return m_controller; }
+ void SetController(ControllerPtr controller);
+
+ void GetControllers(ControllerVector& controllers) const;
+
+ /*!
+ * \brief Address given to the controller's port by the implementation
+ */
+ const std::string& GetPortAddress() const { return m_portAddress; }
+ void SetPortAddress(std::string portAddress);
+
+ /*!
+ * \brief Address given to the controller node by the implementation
+ */
+ const std::string& GetControllerAddress() const { return m_controllerAddress; }
+ void SetControllerAddress(std::string controllerAddress);
+
+ /*!
+ * \brief Collection of ports on this controller
+ *
+ * \return A hub with controller ports, or an empty hub if this controller
+ * has no available ports
+ */
+ const CControllerHub& GetHub() const { return *m_hub; }
+ CControllerHub& GetHub() { return *m_hub; }
+ void SetHub(CControllerHub hub);
+
+ /*!
+ * \brief Check if this node has a valid controller profile
+ */
+ bool IsValid() const { return static_cast<bool>(m_controller); }
+
+ /*!
+ * \brief Check to see if a controller is compatible with a controller port
+ *
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with a port, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& controllerId) const;
+
+ /*!
+ * \brief Check to see if a controller is compatible with a controller port
+ *
+ * \param portAddress The port address
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with a port, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const;
+
+ /*!
+ * \brief Check if this node provides input
+ */
+ bool ProvidesInput() const;
+
+private:
+ ControllerPtr m_controller;
+
+ // Address of the port this controller is connected to
+ std::string m_portAddress;
+
+ // Address of this controller: m_portAddress + "/" + m_controller->ID()
+ std::string m_controllerAddress;
+
+ std::unique_ptr<CControllerHub> m_hub;
+};
+
+using ControllerNodeVec = std::vector<CControllerNode>;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/ControllerTree.h b/xbmc/games/controllers/types/ControllerTree.h
new file mode 100644
index 0000000..47e42e9
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerTree.h
@@ -0,0 +1,30 @@
+/*
+ * 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
+
+//
+// Note: Hierarchy of headers is:
+//
+// - ControllerTree.h (this file)
+// - ControllerHub.h
+// - PortNode.h
+// - ControllerNode.h
+//
+#include "ControllerHub.h"
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Collection of ports on a console
+ */
+using CControllerTree = CControllerHub;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/CMakeLists.txt b/xbmc/games/controllers/windows/CMakeLists.txt
new file mode 100644
index 0000000..72c8154
--- /dev/null
+++ b/xbmc/games/controllers/windows/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES GUIConfigurationWizard.cpp
+ GUIControllerList.cpp
+ GUIControllerWindow.cpp
+ GUIFeatureList.cpp
+)
+
+set(HEADERS GUIConfigurationWizard.h
+ GUIControllerDefines.h
+ GUIControllerList.h
+ GUIControllerWindow.h
+ GUIFeatureList.h
+ IConfigurationWindow.h
+)
+
+core_add_library(games_controller_windows)
diff --git a/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp b/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp
new file mode 100644
index 0000000..c2ac2f5
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2014-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 "GUIConfigurationWizard.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/dialogs/GUIDialogAxisDetection.h"
+#include "games/controllers/guicontrols/GUIFeatureButton.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/IKeymap.h"
+#include "input/InputManager.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IButtonMapCallback.h"
+#include "input/keyboard/KeymapActionMap.h"
+#include "peripherals/Peripherals.h"
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+using namespace std::chrono_literals;
+
+namespace
+{
+
+#define ESC_KEY_CODE 27
+#define SKIPPING_DETECTION_MS 200
+
+// Duration to wait for axes to neutralize after mapping is finished
+constexpr auto POST_MAPPING_WAIT_TIME_MS = 5000ms;
+
+} // namespace
+
+CGUIConfigurationWizard::CGUIConfigurationWizard()
+ : CThread("GUIConfigurationWizard"), m_actionMap(new KEYBOARD::CKeymapActionMap)
+{
+ InitializeState();
+}
+
+CGUIConfigurationWizard::~CGUIConfigurationWizard(void) = default;
+
+void CGUIConfigurationWizard::InitializeState(void)
+{
+ m_currentButton = nullptr;
+ m_cardinalDirection = INPUT::CARDINAL_DIRECTION::NONE;
+ m_wheelDirection = JOYSTICK::WHEEL_DIRECTION::NONE;
+ m_throttleDirection = JOYSTICK::THROTTLE_DIRECTION::NONE;
+ m_history.clear();
+ m_lateAxisDetected = false;
+ m_location.clear();
+}
+
+void CGUIConfigurationWizard::Run(const std::string& strControllerId,
+ const std::vector<IFeatureButton*>& buttons)
+{
+ Abort();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ // Set Run() parameters
+ m_strControllerId = strControllerId;
+ m_buttons = buttons;
+
+ // Reset synchronization variables
+ m_inputEvent.Reset();
+ m_motionlessEvent.Reset();
+ m_bInMotion.clear();
+
+ // Initialize state variables
+ InitializeState();
+ }
+
+ Create();
+}
+
+void CGUIConfigurationWizard::OnUnfocus(IFeatureButton* button)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ if (button == m_currentButton)
+ Abort(false);
+}
+
+bool CGUIConfigurationWizard::Abort(bool bWait /* = true */)
+{
+ bool bWasRunning = !m_bStop;
+
+ StopThread(false);
+
+ m_inputEvent.Set();
+ m_motionlessEvent.Set();
+
+ if (bWait)
+ StopThread(true);
+
+ return bWasRunning;
+}
+
+void CGUIConfigurationWizard::RegisterKey(const CPhysicalFeature& key)
+{
+ if (key.Keycode() != XBMCK_UNKNOWN)
+ m_keyMap[key.Keycode()] = key;
+}
+
+void CGUIConfigurationWizard::UnregisterKeys()
+{
+ m_keyMap.clear();
+}
+
+void CGUIConfigurationWizard::Process(void)
+{
+ CLog::Log(LOGDEBUG, "Starting configuration wizard");
+
+ InstallHooks();
+
+ bool bLateAxisDetected = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+ for (IFeatureButton* button : m_buttons)
+ {
+ // Allow other threads to access the button we're using
+ m_currentButton = button;
+
+ while (!button->IsFinished())
+ {
+ // Allow other threads to access which direction the prompt is on
+ m_cardinalDirection = button->GetCardinalDirection();
+ m_wheelDirection = button->GetWheelDirection();
+ m_throttleDirection = button->GetThrottleDirection();
+
+ // Wait for input
+ {
+ using namespace JOYSTICK;
+
+ CSingleExit exit(m_stateMutex);
+
+ if (button->Feature().Type() == FEATURE_TYPE::UNKNOWN)
+ CLog::Log(LOGDEBUG, "{}: Waiting for input", m_strControllerId);
+ else
+ CLog::Log(LOGDEBUG, "{}: Waiting for input for feature \"{}\"", m_strControllerId,
+ button->Feature().Name());
+
+ if (!button->PromptForInput(m_inputEvent))
+ Abort(false);
+ }
+
+ if (m_bStop)
+ break;
+ }
+
+ button->Reset();
+
+ if (m_bStop)
+ break;
+ }
+
+ bLateAxisDetected = m_lateAxisDetected;
+
+ // Finished mapping
+ InitializeState();
+ }
+
+ for (auto callback : ButtonMapCallbacks())
+ callback.second->SaveButtonMap();
+
+ if (bLateAxisDetected)
+ {
+ CGUIDialogAxisDetection dialog;
+ dialog.Show();
+ }
+ else
+ {
+ // Wait for motion to stop to avoid sending analog actions for the button
+ // that is pressed immediately after button mapping finishes.
+ bool bInMotion;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_motionMutex);
+ bInMotion = !m_bInMotion.empty();
+ }
+
+ if (bInMotion)
+ {
+ CLog::Log(LOGDEBUG, "Configuration wizard: waiting {}ms for axes to neutralize",
+ POST_MAPPING_WAIT_TIME_MS.count());
+ m_motionlessEvent.Wait(POST_MAPPING_WAIT_TIME_MS);
+ }
+ }
+
+ RemoveHooks();
+
+ CLog::Log(LOGDEBUG, "Configuration wizard ended");
+}
+
+bool CGUIConfigurationWizard::MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ using namespace INPUT;
+ using namespace JOYSTICK;
+
+ bool bHandled = false;
+
+ // Abort if another controller cancels the prompt
+ if (IsMapping() && !IsMapping(buttonMap->Location()))
+ {
+ //! @todo This only succeeds for game.controller.default; no actions are
+ // currently defined for other controllers
+ if (keymap)
+ {
+ std::string feature;
+ if (buttonMap->GetFeature(primitive, feature))
+ {
+ const auto& actions = keymap->GetActions(CJoystickUtils::MakeKeyName(feature)).actions;
+ if (!actions.empty())
+ {
+ //! @todo Handle multiple actions mapped to the same key
+ OnAction(actions.begin()->actionId);
+ }
+ }
+ }
+
+ // Discard input
+ bHandled = true;
+ }
+ else if (m_history.find(primitive) != m_history.end())
+ {
+ // Primitive has already been mapped this round, ignore it
+ bHandled = true;
+ }
+ else if (buttonMap->IsIgnored(primitive))
+ {
+ bHandled = true;
+ }
+ else
+ {
+ // Get the current state of the thread
+ IFeatureButton* currentButton;
+ CARDINAL_DIRECTION cardinalDirection;
+ WHEEL_DIRECTION wheelDirection;
+ THROTTLE_DIRECTION throttleDirection;
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+ currentButton = m_currentButton;
+ cardinalDirection = m_cardinalDirection;
+ wheelDirection = m_wheelDirection;
+ throttleDirection = m_throttleDirection;
+ }
+
+ if (currentButton)
+ {
+ // Check if we were expecting a keyboard key
+ if (currentButton->NeedsKey())
+ {
+ if (primitive.Type() == PRIMITIVE_TYPE::KEY)
+ {
+ auto it = m_keyMap.find(primitive.Keycode());
+ if (it != m_keyMap.end())
+ {
+ const CPhysicalFeature& key = it->second;
+ currentButton->SetKey(key);
+ m_inputEvent.Set();
+ }
+ }
+ else
+ {
+ //! @todo Check if primitive is a cancel or motion action
+ }
+ bHandled = true;
+ }
+ else
+ {
+ const CPhysicalFeature& feature = currentButton->Feature();
+
+ if (primitive.Type() == PRIMITIVE_TYPE::RELATIVE_POINTER &&
+ feature.Type() != FEATURE_TYPE::RELPOINTER)
+ {
+ // Don't allow relative pointers to map to other features
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{}: mapping feature \"{}\" for device {} to \"{}\"",
+ m_strControllerId, feature.Name(), buttonMap->Location(), primitive.ToString());
+
+ switch (feature.Type())
+ {
+ case FEATURE_TYPE::SCALAR:
+ {
+ buttonMap->AddScalar(feature.Name(), primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::ANALOG_STICK:
+ {
+ buttonMap->AddAnalogStick(feature.Name(), cardinalDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::RELPOINTER:
+ {
+ buttonMap->AddRelativePointer(feature.Name(), cardinalDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::WHEEL:
+ {
+ buttonMap->AddWheel(feature.Name(), wheelDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::THROTTLE:
+ {
+ buttonMap->AddThrottle(feature.Name(), throttleDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::KEY:
+ {
+ buttonMap->AddKey(feature.Name(), primitive);
+ bHandled = true;
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ if (bHandled)
+ {
+ m_history.insert(primitive);
+
+ // Don't record motion for relative pointers
+ if (primitive.Type() != PRIMITIVE_TYPE::RELATIVE_POINTER)
+ OnMotion(buttonMap);
+
+ m_inputEvent.Set();
+
+ if (m_location.empty())
+ {
+ m_location = buttonMap->Location();
+ m_bIsKeyboard = (primitive.Type() == PRIMITIVE_TYPE::KEY);
+ }
+ }
+ }
+ }
+ }
+
+ return bHandled;
+}
+
+void CGUIConfigurationWizard::OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion)
+{
+ std::unique_lock<CCriticalSection> lock(m_motionMutex);
+
+ if (m_bInMotion.find(buttonMap) != m_bInMotion.end() && !bMotion)
+ OnMotionless(buttonMap);
+}
+
+void CGUIConfigurationWizard::OnLateAxis(const JOYSTICK::IButtonMap* buttonMap,
+ unsigned int axisIndex)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ m_lateAxisDetected = true;
+ Abort(false);
+}
+
+void CGUIConfigurationWizard::OnMotion(const JOYSTICK::IButtonMap* buttonMap)
+{
+ std::unique_lock<CCriticalSection> lock(m_motionMutex);
+
+ m_motionlessEvent.Reset();
+ m_bInMotion.insert(buttonMap);
+}
+
+void CGUIConfigurationWizard::OnMotionless(const JOYSTICK::IButtonMap* buttonMap)
+{
+ m_bInMotion.erase(buttonMap);
+ if (m_bInMotion.empty())
+ m_motionlessEvent.Set();
+}
+
+bool CGUIConfigurationWizard::OnKeyPress(const CKey& key)
+{
+ bool bHandled = false;
+
+ if (!m_bStop)
+ {
+ // Only allow key to abort the prompt if we know for sure that we're mapping
+ // a controller
+ const bool bIsMappingController = (IsMapping() && !m_bIsKeyboard);
+
+ if (bIsMappingController)
+ {
+ bHandled = OnAction(m_actionMap->GetActionID(key));
+ }
+ else
+ {
+ // Allow key press to fall through to the button mapper
+ }
+ }
+
+ return bHandled;
+}
+
+bool CGUIConfigurationWizard::OnAction(unsigned int actionId)
+{
+ bool bHandled = false;
+
+ switch (actionId)
+ {
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_UP:
+ case ACTION_MOVE_DOWN:
+ case ACTION_PAGE_UP:
+ case ACTION_PAGE_DOWN:
+ // Abort and allow motion
+ Abort(false);
+ bHandled = false;
+ break;
+
+ case ACTION_PARENT_DIR:
+ case ACTION_PREVIOUS_MENU:
+ case ACTION_STOP:
+ case ACTION_NAV_BACK:
+ // Abort and prevent action
+ Abort(false);
+ bHandled = true;
+ break;
+
+ default:
+ // Absorb keypress
+ bHandled = true;
+ break;
+ }
+
+ return bHandled;
+}
+
+bool CGUIConfigurationWizard::IsMapping() const
+{
+ return !m_location.empty();
+}
+
+bool CGUIConfigurationWizard::IsMapping(const std::string& location) const
+{
+ return m_location == location;
+}
+
+void CGUIConfigurationWizard::InstallHooks(void)
+{
+ // Install button mapper with lowest priority
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+
+ // Install hook to reattach button mapper when peripherals change
+ CServiceBroker::GetPeripherals().RegisterObserver(this);
+
+ // Install hook to cancel the button mapper
+ CServiceBroker::GetInputManager().RegisterKeyboardDriverHandler(this);
+}
+
+void CGUIConfigurationWizard::RemoveHooks(void)
+{
+ CServiceBroker::GetInputManager().UnregisterKeyboardDriverHandler(this);
+ CServiceBroker::GetPeripherals().UnregisterObserver(this);
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+}
+
+void CGUIConfigurationWizard::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessagePeripheralsChanged:
+ {
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/xbmc/games/controllers/windows/GUIConfigurationWizard.h b/xbmc/games/controllers/windows/GUIConfigurationWizard.h
new file mode 100644
index 0000000..b69badf
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIConfigurationWizard.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2014-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 "IConfigurationWindow.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/XBMC_keysym.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMapper.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/Observer.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+class IActionMap;
+}
+
+namespace GAME
+{
+class CGUIConfigurationWizard : public IConfigurationWizard,
+ public JOYSTICK::IButtonMapper,
+ public KEYBOARD::IKeyboardDriverHandler,
+ public Observer,
+ protected CThread
+{
+public:
+ CGUIConfigurationWizard();
+
+ ~CGUIConfigurationWizard() override;
+
+ // implementation of IConfigurationWizard
+ void Run(const std::string& strControllerId,
+ const std::vector<IFeatureButton*>& buttons) override;
+ void OnUnfocus(IFeatureButton* button) override;
+ bool Abort(bool bWait = true) override;
+ void RegisterKey(const CPhysicalFeature& key) override;
+ void UnregisterKeys() override;
+
+ // implementation of IButtonMapper
+ std::string ControllerID() const override { return m_strControllerId; }
+ bool NeedsCooldown() const override { return true; }
+ bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override { return true; }
+ bool MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion) override;
+ void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override {}
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+private:
+ void InitializeState(void);
+
+ bool IsMapping() const;
+ bool IsMapping(const std::string& location) const;
+
+ void InstallHooks(void);
+ void RemoveHooks(void);
+
+ void OnMotion(const JOYSTICK::IButtonMap* buttonMap);
+ void OnMotionless(const JOYSTICK::IButtonMap* buttonMap);
+
+ bool OnAction(unsigned int actionId);
+
+ // Run() parameters
+ std::string m_strControllerId;
+ std::vector<IFeatureButton*> m_buttons;
+
+ // State variables and mutex
+ IFeatureButton* m_currentButton;
+ INPUT::CARDINAL_DIRECTION m_cardinalDirection;
+ JOYSTICK::WHEEL_DIRECTION m_wheelDirection;
+ JOYSTICK::THROTTLE_DIRECTION m_throttleDirection;
+ std::set<JOYSTICK::CDriverPrimitive> m_history; // History to avoid repeated features
+ bool m_lateAxisDetected; // Set to true if an axis is detected during button mapping
+ std::string m_location; // Peripheral location of device that we're mapping
+ bool m_bIsKeyboard = false; // True if we're mapping keyboard keys
+ CCriticalSection m_stateMutex;
+
+ // Synchronization events
+ CEvent m_inputEvent;
+ CEvent m_motionlessEvent;
+ CCriticalSection m_motionMutex;
+ std::set<const JOYSTICK::IButtonMap*> m_bInMotion;
+
+ // Keyboard handling
+ std::unique_ptr<KEYBOARD::IActionMap> m_actionMap;
+ std::map<XBMCKey, CPhysicalFeature> m_keyMap; // Keycode -> feature
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/GUIControllerDefines.h b/xbmc/games/controllers/windows/GUIControllerDefines.h
new file mode 100644
index 0000000..64d2142
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerDefines.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2014-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
+
+// Duration to wait for input from the user
+#define COUNTDOWN_DURATION_SEC 6
+
+// Warn the user that time is running out after this duration
+#define WAIT_TO_WARN_SEC 2
+
+// GUI Control IDs
+#define CONTROL_CONTROLLER_LIST 3
+#define CONTROL_FEATURE_LIST 5
+#define CONTROL_FEATURE_BUTTON_TEMPLATE 7
+#define CONTROL_FEATURE_GROUP_TITLE 8
+#define CONTROL_FEATURE_SEPARATOR 9
+#define CONTROL_CONTROLLER_BUTTON_TEMPLATE 10
+#define CONTROL_GAME_CONTROLLER 31
+#define CONTROL_CONTROLLER_DESCRIPTION 32
+
+// GUI button IDs
+#define CONTROL_HELP_BUTTON 17
+#define CONTROL_CLOSE_BUTTON 18
+#define CONTROL_RESET_BUTTON 19
+#define CONTROL_GET_MORE 20
+#define CONTROL_FIX_SKIPPING 21
+#define CONTROL_GET_ALL 22
+
+#define MAX_CONTROLLER_COUNT 100 // large enough
+#define MAX_FEATURE_COUNT 200 // large enough
+
+#define CONTROL_CONTROLLER_BUTTONS_START 100
+#define CONTROL_CONTROLLER_BUTTONS_END (CONTROL_CONTROLLER_BUTTONS_START + MAX_CONTROLLER_COUNT)
+#define CONTROL_FEATURE_BUTTONS_START CONTROL_CONTROLLER_BUTTONS_END
+#define CONTROL_FEATURE_BUTTONS_END (CONTROL_FEATURE_BUTTONS_START + MAX_FEATURE_COUNT)
+#define CONTROL_FEATURE_GROUPS_START CONTROL_FEATURE_BUTTONS_END
+#define CONTROL_FEATURE_GROUPS_END (CONTROL_FEATURE_GROUPS_START + MAX_FEATURE_COUNT)
+#define CONTROL_FEATURE_SEPARATORS_START CONTROL_FEATURE_GROUPS_END
+#define CONTROL_FEATURE_SEPARATORS_END (CONTROL_FEATURE_SEPARATORS_START + MAX_FEATURE_COUNT)
diff --git a/xbmc/games/controllers/windows/GUIControllerList.cpp b/xbmc/games/controllers/windows/GUIControllerList.cpp
new file mode 100644
index 0000000..02f8d37
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerList.cpp
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2014-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 "GUIControllerList.h"
+
+#include "GUIControllerDefines.h"
+#include "GUIControllerWindow.h"
+#include "GUIFeatureList.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/GameServices.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerIDs.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/guicontrols/GUIControllerButton.h"
+#include "games/controllers/guicontrols/GUIGameController.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControlGroupList.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindow.h"
+#include "messaging/ApplicationMessenger.h"
+#include "peripherals/Peripherals.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <iterator>
+
+using namespace KODI;
+using namespace ADDON;
+using namespace GAME;
+
+CGUIControllerList::CGUIControllerList(CGUIWindow* window,
+ IFeatureList* featureList,
+ GameClientPtr gameClient)
+ : m_guiWindow(window),
+ m_featureList(featureList),
+ m_controllerList(nullptr),
+ m_controllerButton(nullptr),
+ m_focusedController(-1), // Initially unfocused
+ m_gameClient(std::move(gameClient))
+{
+ assert(m_featureList != nullptr);
+}
+
+bool CGUIControllerList::Initialize(void)
+{
+ m_controllerList =
+ dynamic_cast<CGUIControlGroupList*>(m_guiWindow->GetControl(CONTROL_CONTROLLER_LIST));
+ m_controllerButton =
+ dynamic_cast<CGUIButtonControl*>(m_guiWindow->GetControl(CONTROL_CONTROLLER_BUTTON_TEMPLATE));
+
+ if (m_controllerButton)
+ m_controllerButton->SetVisible(false);
+
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIControllerList::OnEvent);
+ Refresh("");
+
+ return m_controllerList != nullptr && m_controllerButton != nullptr;
+}
+
+void CGUIControllerList::Deinitialize(void)
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ CleanupButtons();
+
+ m_controllerList = nullptr;
+ m_controllerButton = nullptr;
+}
+
+bool CGUIControllerList::Refresh(const std::string& controllerId)
+{
+ // Focus specified controller after refresh
+ std::string focusController = controllerId;
+
+ if (focusController.empty() && m_focusedController >= 0)
+ {
+ // If controller ID wasn't provided, focus current controller
+ focusController = m_controllers[m_focusedController]->ID();
+ }
+
+ if (!RefreshControllers())
+ return false;
+
+ CleanupButtons();
+
+ if (m_controllerList)
+ {
+ unsigned int buttonId = 0;
+ for (const auto& controller : m_controllers)
+ {
+ CGUIButtonControl* pButton =
+ new CGUIControllerButton(*m_controllerButton, controller->Layout().Label(), buttonId++);
+ m_controllerList->AddControl(pButton);
+
+ if (!focusController.empty() && controller->ID() == focusController)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, m_guiWindow->GetID(), pButton->GetID());
+ m_guiWindow->OnMessage(msg);
+ }
+
+ // Just in case
+ if (buttonId >= MAX_CONTROLLER_COUNT)
+ break;
+ }
+ }
+
+ return true;
+}
+
+void CGUIControllerList::OnFocus(unsigned int controllerIndex)
+{
+ if (controllerIndex < m_controllers.size())
+ {
+ m_focusedController = controllerIndex;
+
+ const ControllerPtr& controller = m_controllers[controllerIndex];
+ m_featureList->Load(controller);
+
+ //! @todo Activate controller for all game controller controls
+ CGUIGameController* pController =
+ dynamic_cast<CGUIGameController*>(m_guiWindow->GetControl(CONTROL_GAME_CONTROLLER));
+ if (pController)
+ pController->ActivateController(controller);
+
+ // Update controller description
+ CGUIMessage msg(GUI_MSG_LABEL_SET, m_guiWindow->GetID(), CONTROL_CONTROLLER_DESCRIPTION);
+ msg.SetLabel(controller->Description());
+ m_guiWindow->OnMessage(msg);
+ }
+}
+
+void CGUIControllerList::OnSelect(unsigned int controllerIndex)
+{
+ m_featureList->OnSelect(0);
+}
+
+void CGUIControllerList::ResetController(void)
+{
+ if (0 <= m_focusedController && m_focusedController < (int)m_controllers.size())
+ {
+ const std::string strControllerId = m_controllers[m_focusedController]->ID();
+
+ //! @todo Choose peripheral
+ // For now, ask the user if they would like to reset all peripherals
+ // "Reset controller profile"
+ // "Would you like to reset this controller profile for all devices?"
+ if (!CGUIDialogYesNo::ShowAndGetInput(35060, 35061))
+ return;
+
+ CServiceBroker::GetPeripherals().ResetButtonMaps(strControllerId);
+ }
+}
+
+void CGUIControllerList::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_CONTROLLER_LIST);
+
+ // Focus installed add-on
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled))
+ msg.SetStringParam(event.addonId);
+
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow->GetID());
+ }
+}
+
+bool CGUIControllerList::RefreshControllers(void)
+{
+ // Get current controllers
+ CGameServices& gameServices = CServiceBroker::GetGameServices();
+ ControllerVector newControllers = gameServices.GetControllers();
+
+ // Filter by current game add-on
+ if (m_gameClient)
+ {
+ const CControllerTree& controllers = m_gameClient->Input().GetDefaultControllerTree();
+
+ auto ControllerNotAccepted = [&controllers](const ControllerPtr& controller) {
+ return !controllers.IsControllerAccepted(controller->ID());
+ };
+
+ if (!std::all_of(newControllers.begin(), newControllers.end(), ControllerNotAccepted))
+ newControllers.erase(
+ std::remove_if(newControllers.begin(), newControllers.end(), ControllerNotAccepted),
+ newControllers.end());
+ }
+
+ // Check for changes
+ std::set<std::string> oldControllerIds;
+ std::set<std::string> newControllerIds;
+
+ auto GetControllerID = [](const ControllerPtr& controller) { return controller->ID(); };
+
+ std::transform(m_controllers.begin(), m_controllers.end(),
+ std::inserter(oldControllerIds, oldControllerIds.begin()), GetControllerID);
+ std::transform(newControllers.begin(), newControllers.end(),
+ std::inserter(newControllerIds, newControllerIds.begin()), GetControllerID);
+
+ const bool bChanged = (oldControllerIds != newControllerIds);
+ if (bChanged)
+ {
+ m_controllers = std::move(newControllers);
+
+ // Sort add-ons, with default controller first
+ std::sort(m_controllers.begin(), m_controllers.end(),
+ [](const ControllerPtr& i, const ControllerPtr& j) {
+ if (i->ID() == DEFAULT_CONTROLLER_ID && j->ID() != DEFAULT_CONTROLLER_ID)
+ return true;
+ if (i->ID() != DEFAULT_CONTROLLER_ID && j->ID() == DEFAULT_CONTROLLER_ID)
+ return false;
+
+ return StringUtils::CompareNoCase(i->Layout().Label(), j->Layout().Label()) < 0;
+ });
+ }
+
+ return bChanged;
+}
+
+void CGUIControllerList::CleanupButtons(void)
+{
+ if (m_controllerList)
+ m_controllerList->ClearAll();
+}
diff --git a/xbmc/games/controllers/windows/GUIControllerList.h b/xbmc/games/controllers/windows/GUIControllerList.h
new file mode 100644
index 0000000..8292889
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerList.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014-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 "IConfigurationWindow.h"
+#include "addons/AddonEvents.h"
+#include "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <set>
+#include <string>
+
+class CGUIButtonControl;
+class CGUIControlGroupList;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIControllerWindow;
+
+class CGUIControllerList : public IControllerList
+{
+public:
+ CGUIControllerList(CGUIWindow* window, IFeatureList* featureList, GameClientPtr gameClient);
+ ~CGUIControllerList() override { Deinitialize(); }
+
+ // implementation of IControllerList
+ bool Initialize() override;
+ void Deinitialize() override;
+ bool Refresh(const std::string& controllerId) override;
+ void OnFocus(unsigned int controllerIndex) override;
+ void OnSelect(unsigned int controllerIndex) override;
+ int GetFocusedController() const override { return m_focusedController; }
+ void ResetController() override;
+
+private:
+ bool RefreshControllers(void);
+
+ void CleanupButtons(void);
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ // GUI stuff
+ CGUIWindow* const m_guiWindow;
+ IFeatureList* const m_featureList;
+ CGUIControlGroupList* m_controllerList;
+ CGUIButtonControl* m_controllerButton;
+
+ // Game stuff
+ ControllerVector m_controllers;
+ int m_focusedController;
+ GameClientPtr m_gameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/GUIControllerWindow.cpp b/xbmc/games/controllers/windows/GUIControllerWindow.cpp
new file mode 100644
index 0000000..6b2d2ac
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerWindow.cpp
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2014-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 "GUIControllerWindow.h"
+
+#include "GUIControllerDefines.h"
+#include "GUIControllerList.h"
+#include "GUIFeatureList.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "games/addons/GameClient.h"
+#include "games/controllers/dialogs/ControllerInstaller.h"
+#include "games/controllers/dialogs/GUIDialogIgnoreInput.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+
+// To enable button mapping support
+#include "peripherals/Peripherals.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace KODI::MESSAGING;
+
+CGUIControllerWindow::CGUIControllerWindow(void)
+ : CGUIDialog(WINDOW_DIALOG_GAME_CONTROLLERS, "DialogGameControllers.xml"),
+ m_installer(new CControllerInstaller)
+{
+ // initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIControllerWindow::~CGUIControllerWindow(void)
+{
+ delete m_controllerList;
+ delete m_featureList;
+}
+
+void CGUIControllerWindow::DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ /*
+ * Apply the faded focus texture to the current controller when unfocused
+ */
+
+ CGUIControl* control = nullptr; // The controller button
+ bool bAlphaFaded = false; // True if the controller button has been focused and faded this frame
+
+ if (m_controllerList && m_controllerList->GetFocusedController() >= 0)
+ {
+ control = GetFirstFocusableControl(CONTROL_CONTROLLER_BUTTONS_START +
+ m_controllerList->GetFocusedController());
+ if (control && !control->HasFocus())
+ {
+ if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON)
+ {
+ control->SetFocus(true);
+ static_cast<CGUIButtonControl*>(control)->SetAlpha(0x80);
+ bAlphaFaded = true;
+ }
+ }
+ }
+
+ CGUIDialog::DoProcess(currentTime, dirtyregions);
+
+ if (control && bAlphaFaded)
+ {
+ control->SetFocus(false);
+ if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON)
+ static_cast<CGUIButtonControl*>(control)->SetAlpha(0xFF);
+ }
+}
+
+bool CGUIControllerWindow::OnMessage(CGUIMessage& message)
+{
+ // Set to true to block the call to the super class
+ bool bHandled = false;
+
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int controlId = message.GetSenderId();
+
+ if (controlId == CONTROL_CLOSE_BUTTON)
+ {
+ Close();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_GET_MORE)
+ {
+ GetMoreControllers();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_GET_ALL)
+ {
+ GetAllControllers();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_RESET_BUTTON)
+ {
+ ResetController();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_HELP_BUTTON)
+ {
+ ShowHelp();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_FIX_SKIPPING)
+ {
+ ShowButtonCaptureDialog();
+ }
+ else if (CONTROL_CONTROLLER_BUTTONS_START <= controlId &&
+ controlId < CONTROL_CONTROLLER_BUTTONS_END)
+ {
+ OnControllerSelected(controlId - CONTROL_CONTROLLER_BUTTONS_START);
+ bHandled = true;
+ }
+ else if (CONTROL_FEATURE_BUTTONS_START <= controlId &&
+ controlId < CONTROL_FEATURE_BUTTONS_END)
+ {
+ OnFeatureSelected(controlId - CONTROL_FEATURE_BUTTONS_START);
+ bHandled = true;
+ }
+ break;
+ }
+ case GUI_MSG_FOCUSED:
+ {
+ int controlId = message.GetControlId();
+
+ if (CONTROL_CONTROLLER_BUTTONS_START <= controlId &&
+ controlId < CONTROL_CONTROLLER_BUTTONS_END)
+ {
+ OnControllerFocused(controlId - CONTROL_CONTROLLER_BUTTONS_START);
+ }
+ else if (CONTROL_FEATURE_BUTTONS_START <= controlId &&
+ controlId < CONTROL_FEATURE_BUTTONS_END)
+ {
+ OnFeatureFocused(controlId - CONTROL_FEATURE_BUTTONS_START);
+ }
+ break;
+ }
+ case GUI_MSG_SETFOCUS:
+ {
+ int controlId = message.GetControlId();
+
+ if (CONTROL_CONTROLLER_BUTTONS_START <= controlId &&
+ controlId < CONTROL_CONTROLLER_BUTTONS_END)
+ {
+ OnControllerFocused(controlId - CONTROL_CONTROLLER_BUTTONS_START);
+ }
+ else if (CONTROL_FEATURE_BUTTONS_START <= controlId &&
+ controlId < CONTROL_FEATURE_BUTTONS_END)
+ {
+ OnFeatureFocused(controlId - CONTROL_FEATURE_BUTTONS_START);
+ }
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ int controlId = message.GetControlId();
+
+ if (controlId == CONTROL_CONTROLLER_LIST)
+ {
+ const std::string controllerId = message.GetStringParam();
+ if (m_controllerList && m_controllerList->Refresh(controllerId))
+ {
+ CGUIDialog::OnMessage(message);
+ bHandled = true;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!bHandled)
+ bHandled = CGUIDialog::OnMessage(message);
+
+ return bHandled;
+}
+
+void CGUIControllerWindow::OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event)
+{
+ UpdateButtons();
+}
+
+void CGUIControllerWindow::OnEvent(const ADDON::AddonEvent& event)
+{
+ using namespace ADDON;
+
+ if (typeid(event) == typeid(AddonEvents::Enabled) || // also called on install,
+ typeid(event) == typeid(AddonEvents::Disabled) || // not called on uninstall
+ typeid(event) == typeid(AddonEvents::UnInstalled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled))
+ {
+ if (CServiceBroker::GetAddonMgr().HasType(event.addonId, AddonType::GAME_CONTROLLER))
+ {
+ UpdateButtons();
+ }
+ }
+}
+
+void CGUIControllerWindow::OnInitWindow(void)
+{
+ // 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);
+
+ CGUIDialog::OnInitWindow();
+
+ if (!m_featureList)
+ {
+ m_featureList = new CGUIFeatureList(this, m_gameClient);
+ if (!m_featureList->Initialize())
+ {
+ delete m_featureList;
+ m_featureList = nullptr;
+ }
+ }
+
+ if (!m_controllerList && m_featureList)
+ {
+ m_controllerList = new CGUIControllerList(this, m_featureList, m_gameClient);
+ if (!m_controllerList->Initialize())
+ {
+ delete m_controllerList;
+ m_controllerList = nullptr;
+ }
+ }
+
+ // Focus the first controller so that the feature list is loaded properly
+ CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), CONTROL_CONTROLLER_BUTTONS_START);
+ OnMessage(msgFocus);
+
+ // Enable button mapping support
+ CServiceBroker::GetPeripherals().EnableButtonMapping();
+
+ UpdateButtons();
+
+ // subscribe to events
+ CServiceBroker::GetRepositoryUpdater().Events().Subscribe(this, &CGUIControllerWindow::OnEvent);
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIControllerWindow::OnEvent);
+}
+
+void CGUIControllerWindow::OnDeinitWindow(int nextWindowID)
+{
+ CServiceBroker::GetRepositoryUpdater().Events().Unsubscribe(this);
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ if (m_controllerList)
+ {
+ m_controllerList->Deinitialize();
+ delete m_controllerList;
+ m_controllerList = nullptr;
+ }
+
+ if (m_featureList)
+ {
+ m_featureList->Deinitialize();
+ delete m_featureList;
+ m_featureList = nullptr;
+ }
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ m_gameClient.reset();
+}
+
+void CGUIControllerWindow::OnControllerFocused(unsigned int controllerIndex)
+{
+ if (m_controllerList)
+ m_controllerList->OnFocus(controllerIndex);
+}
+
+void CGUIControllerWindow::OnControllerSelected(unsigned int controllerIndex)
+{
+ if (m_controllerList)
+ m_controllerList->OnSelect(controllerIndex);
+}
+
+void CGUIControllerWindow::OnFeatureFocused(unsigned int buttonIndex)
+{
+ if (m_featureList)
+ m_featureList->OnFocus(buttonIndex);
+}
+
+void CGUIControllerWindow::OnFeatureSelected(unsigned int buttonIndex)
+{
+ if (m_featureList)
+ m_featureList->OnSelect(buttonIndex);
+}
+
+void CGUIControllerWindow::UpdateButtons(void)
+{
+ using namespace ADDON;
+
+ VECADDONS addons;
+ if (m_gameClient)
+ {
+ SET_CONTROL_HIDDEN(CONTROL_GET_MORE);
+ SET_CONTROL_HIDDEN(CONTROL_GET_ALL);
+ }
+ else
+ {
+ const bool bEnable = CServiceBroker::GetAddonMgr().GetInstallableAddons(
+ addons, ADDON::AddonType::GAME_CONTROLLER) &&
+ !addons.empty();
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_GET_MORE, bEnable);
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_GET_ALL, bEnable);
+ }
+}
+
+void CGUIControllerWindow::GetMoreControllers(void)
+{
+ std::string strAddonId;
+ if (CGUIWindowAddonBrowser::SelectAddonID(ADDON::AddonType::GAME_CONTROLLER, strAddonId, false,
+ true, false, true, false) < 0)
+ {
+ // "Controller profiles"
+ // "All available controller profiles are installed."
+ HELPERS::ShowOKDialogText(CVariant{35050}, CVariant{35062});
+ return;
+ }
+}
+
+void CGUIControllerWindow::GetAllControllers()
+{
+ if (m_installer->IsRunning())
+ return;
+
+ m_installer->Create(false);
+}
+
+void CGUIControllerWindow::ResetController(void)
+{
+ if (m_controllerList)
+ m_controllerList->ResetController();
+}
+
+void CGUIControllerWindow::ShowHelp(void)
+{
+ // "Help"
+ // <help text>
+ HELPERS::ShowOKDialogText(CVariant{10043}, CVariant{35055});
+}
+
+void CGUIControllerWindow::ShowButtonCaptureDialog(void)
+{
+ CGUIDialogIgnoreInput dialog;
+ dialog.Show();
+}
diff --git a/xbmc/games/controllers/windows/GUIControllerWindow.h b/xbmc/games/controllers/windows/GUIControllerWindow.h
new file mode 100644
index 0000000..137fd56
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerWindow.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014-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 "addons/RepositoryUpdater.h"
+#include "games/GameTypes.h"
+#include "guilib/GUIDialog.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerInstaller;
+class IControllerList;
+class IFeatureList;
+
+class CGUIControllerWindow : public CGUIDialog
+{
+public:
+ CGUIControllerWindow(void);
+ ~CGUIControllerWindow() override;
+
+ // implementation of CGUIControl via CGUIDialog
+ void DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // implementation of CGUIWindow via CGUIDialog
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+private:
+ void OnControllerFocused(unsigned int controllerIndex);
+ void OnControllerSelected(unsigned int controllerIndex);
+ void OnFeatureFocused(unsigned int featureIndex);
+ void OnFeatureSelected(unsigned int featureIndex);
+ void UpdateButtons(void);
+
+ // Callbacks for events
+ void OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event);
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ // Action for the available button
+ void GetMoreControllers(void);
+ void GetAllControllers();
+ void ResetController(void);
+ void ShowHelp(void);
+ void ShowButtonCaptureDialog(void);
+
+ IControllerList* m_controllerList = nullptr;
+ IFeatureList* m_featureList = nullptr;
+
+ // Game parameters
+ GameClientPtr m_gameClient;
+
+ // Controller parameters
+ std::unique_ptr<CControllerInstaller> m_installer;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/GUIFeatureList.cpp b/xbmc/games/controllers/windows/GUIFeatureList.cpp
new file mode 100644
index 0000000..d43c16d
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIFeatureList.cpp
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2014-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 "GUIFeatureList.h"
+
+#include "GUIConfigurationWizard.h"
+#include "GUIControllerDefines.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/guicontrols/GUIFeatureButton.h"
+#include "games/controllers/guicontrols/GUIFeatureControls.h"
+#include "games/controllers/guicontrols/GUIFeatureFactory.h"
+#include "games/controllers/guicontrols/GUIFeatureTranslator.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControlGroupList.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/LocalizeStrings.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIFeatureList::CGUIFeatureList(CGUIWindow* window, GameClientPtr gameClient)
+ : m_window(window),
+ m_guiList(nullptr),
+ m_guiButtonTemplate(nullptr),
+ m_guiGroupTitle(nullptr),
+ m_guiFeatureSeparator(nullptr),
+ m_gameClient(std::move(gameClient)),
+ m_wizard(new CGUIConfigurationWizard)
+{
+}
+
+CGUIFeatureList::~CGUIFeatureList(void)
+{
+ Deinitialize();
+ delete m_wizard;
+}
+
+bool CGUIFeatureList::Initialize(void)
+{
+ m_guiList = dynamic_cast<CGUIControlGroupList*>(m_window->GetControl(CONTROL_FEATURE_LIST));
+ m_guiButtonTemplate =
+ dynamic_cast<CGUIButtonControl*>(m_window->GetControl(CONTROL_FEATURE_BUTTON_TEMPLATE));
+ m_guiGroupTitle =
+ dynamic_cast<CGUILabelControl*>(m_window->GetControl(CONTROL_FEATURE_GROUP_TITLE));
+ m_guiFeatureSeparator = dynamic_cast<CGUIImage*>(m_window->GetControl(CONTROL_FEATURE_SEPARATOR));
+
+ if (m_guiButtonTemplate)
+ m_guiButtonTemplate->SetVisible(false);
+
+ if (m_guiGroupTitle)
+ m_guiGroupTitle->SetVisible(false);
+
+ if (m_guiFeatureSeparator)
+ m_guiFeatureSeparator->SetVisible(false);
+
+ return m_guiList != nullptr && m_guiButtonTemplate != nullptr;
+}
+
+void CGUIFeatureList::Deinitialize(void)
+{
+ CleanupButtons();
+
+ m_guiList = nullptr;
+ m_guiButtonTemplate = nullptr;
+ m_guiGroupTitle = nullptr;
+ m_guiFeatureSeparator = nullptr;
+}
+
+void CGUIFeatureList::Load(const ControllerPtr& controller)
+{
+ if (m_controller && m_controller->ID() == controller->ID())
+ return; // Already loaded
+
+ CleanupButtons();
+
+ // Set new controller
+ m_controller = controller;
+
+ // Get features
+ const std::vector<CPhysicalFeature>& features = controller->Features();
+
+ // Split into groups
+ auto featureGroups = GetFeatureGroups(features);
+
+ // Create controls
+ m_buttonCount = 0;
+ for (auto itGroup = featureGroups.begin(); itGroup != featureGroups.end(); ++itGroup)
+ {
+ const std::string& groupName = itGroup->groupName;
+ const bool bIsVirtualKey = itGroup->bIsVirtualKey;
+
+ std::vector<CGUIButtonControl*> buttons;
+
+ // Create buttons
+ if (bIsVirtualKey)
+ {
+ CGUIButtonControl* button = GetSelectKeyButton(itGroup->features, m_buttonCount);
+ if (button != nullptr)
+ buttons.push_back(button);
+ }
+ else
+ {
+ buttons = GetButtons(itGroup->features, m_buttonCount);
+ }
+
+ // Just in case
+ if (m_buttonCount + buttons.size() >= MAX_FEATURE_COUNT)
+ break;
+
+ // Add a separator if the group list isn't empty
+ if (m_guiFeatureSeparator && m_guiList->GetTotalSize() > 0)
+ {
+ CGUIFeatureSeparator* pSeparator =
+ new CGUIFeatureSeparator(*m_guiFeatureSeparator, m_buttonCount);
+ m_guiList->AddControl(pSeparator);
+ }
+
+ // Add the group title
+ if (m_guiGroupTitle && !groupName.empty())
+ {
+ CGUIFeatureGroupTitle* pGroupTitle =
+ new CGUIFeatureGroupTitle(*m_guiGroupTitle, groupName, m_buttonCount);
+ m_guiList->AddControl(pGroupTitle);
+ }
+
+ // Add the buttons
+ for (CGUIButtonControl* pButton : buttons)
+ m_guiList->AddControl(pButton);
+
+ m_buttonCount += static_cast<unsigned int>(buttons.size());
+ }
+}
+
+void CGUIFeatureList::OnSelect(unsigned int buttonIndex)
+{
+ // Generate list of buttons for the wizard
+ std::vector<IFeatureButton*> buttons;
+ for (; buttonIndex < m_buttonCount; buttonIndex++)
+ {
+ IFeatureButton* control = GetButtonControl(buttonIndex);
+ if (control == nullptr)
+ continue;
+
+ if (control->AllowWizard())
+ buttons.push_back(control);
+ else
+ {
+ // Only map this button if it's the only one
+ if (buttons.empty())
+ buttons.push_back(control);
+ break;
+ }
+ }
+
+ m_wizard->Run(m_controller->ID(), buttons);
+}
+
+IFeatureButton* CGUIFeatureList::GetButtonControl(unsigned int buttonIndex)
+{
+ CGUIControl* control = m_guiList->GetControl(CONTROL_FEATURE_BUTTONS_START + buttonIndex);
+
+ return static_cast<IFeatureButton*>(dynamic_cast<CGUIFeatureButton*>(control));
+}
+
+void CGUIFeatureList::CleanupButtons(void)
+{
+ m_buttonCount = 0;
+
+ m_wizard->Abort(true);
+ m_wizard->UnregisterKeys();
+
+ if (m_guiList)
+ m_guiList->ClearAll();
+}
+
+std::vector<CGUIFeatureList::FeatureGroup> CGUIFeatureList::GetFeatureGroups(
+ const std::vector<CPhysicalFeature>& features) const
+{
+ std::vector<FeatureGroup> groups;
+
+ // Get group names
+ std::vector<std::string> groupNames;
+ for (const CPhysicalFeature& feature : features)
+ {
+ // Skip features not supported by the game client
+ if (m_gameClient)
+ {
+ if (!m_gameClient->Input().HasFeature(m_controller->ID(), feature.Name()))
+ continue;
+ }
+
+ bool bAdded = false;
+
+ if (!groups.empty())
+ {
+ FeatureGroup& previousGroup = *groups.rbegin();
+ if (feature.CategoryLabel() == previousGroup.groupName)
+ {
+ // Add feature to previous group
+ previousGroup.features.emplace_back(feature);
+ bAdded = true;
+
+ // If feature is a key, add it to the preceding virtual group as well
+ if (feature.Category() == JOYSTICK::FEATURE_CATEGORY::KEY && groups.size() >= 2)
+ {
+ FeatureGroup& virtualGroup = *(groups.rbegin() + 1);
+ if (virtualGroup.bIsVirtualKey)
+ virtualGroup.features.emplace_back(feature);
+ }
+ }
+ }
+
+ if (!bAdded)
+ {
+ // If feature is a key, create a virtual group that allows the user to
+ // select which key to map
+ if (feature.Category() == JOYSTICK::FEATURE_CATEGORY::KEY)
+ {
+ FeatureGroup virtualGroup;
+ virtualGroup.groupName = g_localizeStrings.Get(35166); // "All keys"
+ virtualGroup.bIsVirtualKey = true;
+ virtualGroup.features.emplace_back(feature);
+ groups.emplace_back(std::move(virtualGroup));
+ }
+
+ // Create new group and add feature
+ FeatureGroup group;
+ group.groupName = feature.CategoryLabel();
+ group.features.emplace_back(feature);
+ groups.emplace_back(std::move(group));
+ }
+ }
+
+ // If there are no features, add an empty group
+ if (groups.empty())
+ {
+ FeatureGroup group;
+ group.groupName = g_localizeStrings.Get(35022); // "Nothing to map"
+ groups.emplace_back(std::move(group));
+ }
+
+ return groups;
+}
+
+bool CGUIFeatureList::HasButton(JOYSTICK::FEATURE_TYPE type) const
+{
+ return CGUIFeatureTranslator::GetButtonType(type) != BUTTON_TYPE::UNKNOWN;
+}
+
+std::vector<CGUIButtonControl*> CGUIFeatureList::GetButtons(
+ const std::vector<CPhysicalFeature>& features, unsigned int startIndex)
+{
+ std::vector<CGUIButtonControl*> buttons;
+
+ // Create buttons
+ unsigned int buttonIndex = startIndex;
+ for (const CPhysicalFeature& feature : features)
+ {
+ BUTTON_TYPE buttonType = CGUIFeatureTranslator::GetButtonType(feature.Type());
+
+ CGUIButtonControl* pButton = CGUIFeatureFactory::CreateButton(buttonType, *m_guiButtonTemplate,
+ m_wizard, feature, buttonIndex);
+
+ // If successful, add button to result
+ if (pButton != nullptr)
+ {
+ buttons.push_back(pButton);
+ buttonIndex++;
+ }
+ }
+
+ return buttons;
+}
+
+CGUIButtonControl* CGUIFeatureList::GetSelectKeyButton(
+ const std::vector<CPhysicalFeature>& features, unsigned int buttonIndex)
+{
+ // Expose keycodes to the wizard
+ for (const CPhysicalFeature& feature : features)
+ {
+ if (feature.Type() == JOYSTICK::FEATURE_TYPE::KEY)
+ m_wizard->RegisterKey(feature);
+ }
+
+ return CGUIFeatureFactory::CreateButton(BUTTON_TYPE::SELECT_KEY, *m_guiButtonTemplate, m_wizard,
+ CPhysicalFeature(), buttonIndex);
+}
diff --git a/xbmc/games/controllers/windows/GUIFeatureList.h b/xbmc/games/controllers/windows/GUIFeatureList.h
new file mode 100644
index 0000000..c7df54f
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIFeatureList.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014-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 "IConfigurationWindow.h"
+#include "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/joysticks/JoystickTypes.h"
+
+class CGUIButtonControl;
+class CGUIControlGroupList;
+class CGUIImage;
+class CGUILabelControl;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureList : public IFeatureList
+{
+public:
+ CGUIFeatureList(CGUIWindow* window, GameClientPtr gameClient);
+ ~CGUIFeatureList() override;
+
+ // implementation of IFeatureList
+ bool Initialize() override;
+ void Deinitialize() override;
+ bool HasButton(JOYSTICK::FEATURE_TYPE type) const override;
+ void Load(const ControllerPtr& controller) override;
+ void OnFocus(unsigned int buttonIndex) override {}
+ void OnSelect(unsigned int buttonIndex) override;
+
+private:
+ IFeatureButton* GetButtonControl(unsigned int buttonIndex);
+
+ void CleanupButtons(void);
+
+ // Helper functions
+ struct FeatureGroup
+ {
+ std::string groupName;
+ std::vector<CPhysicalFeature> features;
+ /*!
+ * True if this group is a button that allows the user to map a key of
+ * their choosing.
+ */
+ bool bIsVirtualKey = false;
+ };
+ std::vector<FeatureGroup> GetFeatureGroups(const std::vector<CPhysicalFeature>& features) const;
+ std::vector<CGUIButtonControl*> GetButtons(const std::vector<CPhysicalFeature>& features,
+ unsigned int startIndex);
+ CGUIButtonControl* GetSelectKeyButton(const std::vector<CPhysicalFeature>& features,
+ unsigned int buttonIndex);
+
+ // GUI stuff
+ CGUIWindow* const m_window;
+ unsigned int m_buttonCount = 0;
+ CGUIControlGroupList* m_guiList;
+ CGUIButtonControl* m_guiButtonTemplate;
+ CGUILabelControl* m_guiGroupTitle;
+ CGUIImage* m_guiFeatureSeparator;
+
+ // Game window stuff
+ GameClientPtr m_gameClient;
+ ControllerPtr m_controller;
+ IConfigurationWizard* m_wizard;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/IConfigurationWindow.h b/xbmc/games/controllers/windows/IConfigurationWindow.h
new file mode 100644
index 0000000..017adf0
--- /dev/null
+++ b/xbmc/games/controllers/windows/IConfigurationWindow.h
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2014-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 "games/controllers/ControllerTypes.h"
+#include "input/InputTypes.h"
+#include "input/joysticks/JoystickTypes.h"
+
+#include <string>
+#include <vector>
+
+class CEvent;
+
+/*!
+ * \brief Controller configuration window
+ *
+ * The configuration window presents a list of controllers. Also on the screen
+ * is a list of features belonging to that controller.
+ *
+ * The configuration utility reacts to several events:
+ *
+ * 1) When a controller is focused, the feature list is populated with the
+ * controller's features.
+ *
+ * 2) When a feature is selected, the user is prompted for controller input.
+ * This initiates a "wizard" that walks the user through the subsequent
+ * features.
+ *
+ * 3) When the wizard's active feature loses focus, the wizard is cancelled
+ * and the prompt for input ends.
+ */
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalFeature;
+
+/*!
+ * \brief A list populated by installed controllers
+ */
+class IControllerList
+{
+public:
+ virtual ~IControllerList() = default;
+
+ /*!
+ * \brief Initialize the resource
+ * \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(void) = 0;
+
+ /*!
+ * \brief Deinitialize the resource
+ */
+ virtual void Deinitialize(void) = 0;
+
+ /*!
+ * \brief Refresh the contents of the list
+ * \param controllerId The controller to focus, or empty to leave focus unchanged
+ * \return True if the list was changed
+ */
+ virtual bool Refresh(const std::string& controllerId) = 0;
+
+ /*
+ * \brief The specified controller has been focused
+ * \param controllerIndex The index of the controller being focused
+ */
+ virtual void OnFocus(unsigned int controllerIndex) = 0;
+
+ /*!
+ * \brief The specified controller has been selected
+ * \param controllerIndex The index of the controller being selected
+ */
+ virtual void OnSelect(unsigned int controllerIndex) = 0;
+
+ /*!
+ * \brief Get the index of the focused controller
+ * \return The index of the focused controller, or -1 if no controller has been focused yet
+ */
+ virtual int GetFocusedController() const = 0;
+
+ /*!
+ * \brief Reset the focused controller
+ */
+ virtual void ResetController(void) = 0;
+};
+
+/*!
+ * \brief A list populated by the controller's features
+ */
+class IFeatureList
+{
+public:
+ virtual ~IFeatureList() = default;
+
+ /*!
+ * \brief Initialize the resource
+ * \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(void) = 0;
+
+ /*!
+ * \brief Deinitialize the resource
+ * \remark This must be called if Initialize() returned true
+ */
+ virtual void Deinitialize(void) = 0;
+
+ /*!
+ * \brief Check if the feature type has any buttons in the GUI
+ * \param The type of the feature being added to the GUI
+ * \return True if the type is support, false otherwise
+ */
+ virtual bool HasButton(JOYSTICK::FEATURE_TYPE type) const = 0;
+
+ /*!
+ * \brief Load the features for the specified controller
+ * \param controller The controller to load
+ */
+ virtual void Load(const ControllerPtr& controller) = 0;
+
+ /*!
+ * \brief Focus has been set to the specified GUI button
+ * \param buttonIndex The index of the button being focused
+ */
+ virtual void OnFocus(unsigned int buttonIndex) = 0;
+
+ /*!
+ * \brief The specified GUI button has been selected
+ * \param buttonIndex The index of the button being selected
+ */
+ virtual void OnSelect(unsigned int buttonIndex) = 0;
+};
+
+/*!
+ * \brief A GUI button in a feature list
+ */
+class IFeatureButton
+{
+public:
+ virtual ~IFeatureButton() = default;
+
+ /*!
+ * \brief Get the feature represented by this button
+ */
+ virtual const CPhysicalFeature& Feature(void) const = 0;
+
+ /*!
+ * \brief Allow the wizard to include this feature in a list of buttons
+ * to map
+ */
+ virtual bool AllowWizard() const { return true; }
+
+ /*!
+ * \brief Prompt the user for a single input element
+ * \param waitEvent The event to block on while prompting for input
+ * \return true if input was received (event fired), false if the prompt timed out
+ *
+ * After the button has finished prompting the user for all the input
+ * elements it requires, this will return false until Reset() is called.
+ */
+ virtual bool PromptForInput(CEvent& waitEvent) = 0;
+
+ /*!
+ * \brief Check if the button supports further calls to PromptForInput()
+ * \return true if the button requires no more input elements from the user
+ */
+ virtual bool IsFinished(void) const = 0;
+
+ /*!
+ * \brief Get the direction of the next analog stick or relative pointer
+ * prompt
+ * \return The next direction to be prompted, or UNKNOWN if this isn't a
+ * cardinal feature or the prompt is finished
+ */
+ virtual INPUT::CARDINAL_DIRECTION GetCardinalDirection(void) const = 0;
+
+ /*!
+ * \brief Get the direction of the next wheel prompt
+ * \return The next direction to be prompted, or UNKNOWN if this isn't a
+ * wheel or the prompt is finished
+ */
+ virtual JOYSTICK::WHEEL_DIRECTION GetWheelDirection(void) const = 0;
+
+ /*!
+ * \brief Get the direction of the next throttle prompt
+ * \return The next direction to be prompted, or UNKNOWN if this isn't a
+ * throttle or the prompt is finished
+ */
+ virtual JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection(void) const = 0;
+
+ /*!
+ * \brief True if the button is waiting for a key press
+ */
+ virtual bool NeedsKey() const { return false; }
+
+ /*!
+ * \brief Set the pressed key that the user will be prompted to map
+ *
+ * \param key The key that was pressed
+ */
+ virtual void SetKey(const CPhysicalFeature& key) {}
+
+ /*!
+ * \brief Reset button after prompting for input has finished
+ */
+ virtual void Reset(void) = 0;
+};
+
+/*!
+ * \brief A wizard to direct user input
+ */
+class IConfigurationWizard
+{
+public:
+ virtual ~IConfigurationWizard() = default;
+
+ /*!
+ * \brief Start the wizard for the specified buttons
+ * \param controllerId The controller ID being mapped
+ * \param buttons The buttons to map
+ */
+ virtual void Run(const std::string& strControllerId,
+ const std::vector<IFeatureButton*>& buttons) = 0;
+
+ /*!
+ * \brief Callback for feature losing focus
+ * \param button The feature button losing focus
+ */
+ virtual void OnUnfocus(IFeatureButton* button) = 0;
+
+ /*!
+ * \brief Abort a running wizard
+ * \param bWait True if the call should block until the wizard is fully aborted
+ * \return true if aborted, or false if the wizard wasn't running
+ */
+ virtual bool Abort(bool bWait = true) = 0;
+
+ /*!
+ * \brief Register a key by its keycode
+ * \param key A key with a valid keycode
+ *
+ * This should be called before Run(). It allows the user to choose a key
+ * to map instead of scrolling through a long list.
+ */
+ virtual void RegisterKey(const CPhysicalFeature& key) = 0;
+
+ /*!
+ * \brief Unregister all registered keys
+ */
+ virtual void UnregisterKeys() = 0;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/CMakeLists.txt b/xbmc/games/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..d6c9a16
--- /dev/null
+++ b/xbmc/games/dialogs/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES GUIDialogSelectGameClient.cpp
+ GUIDialogSelectSavestate.cpp
+)
+
+set(HEADERS DialogGameDefines.h
+ GUIDialogSelectGameClient.h
+ GUIDialogSelectSavestate.h
+)
+
+core_add_library(gamedialogs)
diff --git a/xbmc/games/dialogs/DialogGameDefines.h b/xbmc/games/dialogs/DialogGameDefines.h
new file mode 100644
index 0000000..60dc99d
--- /dev/null
+++ b/xbmc/games/dialogs/DialogGameDefines.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020-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
+
+// Name of list item property for savestate captions
+constexpr auto SAVESTATE_LABEL = "savestate.label";
+constexpr auto SAVESTATE_CAPTION = "savestate.caption";
+constexpr auto SAVESTATE_GAME_CLIENT = "savestate.gameclient";
+
+// Control IDs for game dialogs
+constexpr unsigned int CONTROL_VIDEO_HEADING = 10810;
+constexpr unsigned int CONTROL_VIDEO_THUMBS = 10811;
+constexpr unsigned int CONTROL_VIDEO_DESCRIPTION = 10812;
+constexpr unsigned int CONTROL_SAVES_HEADING = 10820;
+constexpr unsigned int CONTROL_SAVES_DETAILED_LIST = 3; // Select dialog defaults to this control ID
+constexpr unsigned int CONTROL_SAVES_DESCRIPTION = 10822;
+constexpr unsigned int CONTROL_SAVES_EMULATOR_NAME = 10823;
+constexpr unsigned int CONTROL_SAVES_EMULATOR_ICON = 10824;
+constexpr unsigned int CONTROL_SAVES_NEW_BUTTON = 10825;
+constexpr unsigned int CONTROL_SAVES_CANCEL_BUTTON = 10826;
+constexpr unsigned int CONTROL_NUMBER_OF_ITEMS = 10827;
diff --git a/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp
new file mode 100644
index 0000000..e17bc27
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016-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 "GUIDialogSelectGameClient.h"
+
+#include "FileItem.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "filesystem/AddonsDirectory.h"
+#include "games/addons/GameClient.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace KODI::MESSAGING;
+using namespace GAME;
+
+std::string CGUIDialogSelectGameClient::ShowAndGetGameClient(const std::string& gamePath,
+ const GameClientVector& candidates,
+ const GameClientVector& installable)
+{
+ std::string gameClient;
+
+ LogGameClients(candidates, installable);
+
+ std::string extension = URIUtils::GetExtension(gamePath);
+
+ // "Select emulator for {0:s}"
+ CGUIDialogSelect* dialog =
+ GetDialog(StringUtils::Format(g_localizeStrings.Get(35258), extension));
+ if (dialog != nullptr)
+ {
+ // Turn the addons into items
+ CFileItemList items;
+ CFileItemList installableItems;
+ for (const auto& candidate : candidates)
+ {
+ CFileItemPtr item(XFILE::CAddonsDirectory::FileItemFromAddon(candidate, candidate->ID()));
+ item->SetLabel2(g_localizeStrings.Get(35257)); // "Installed"
+ items.Add(std::move(item));
+ }
+ for (const auto& addon : installable)
+ {
+ CFileItemPtr item(XFILE::CAddonsDirectory::FileItemFromAddon(addon, addon->ID()));
+ installableItems.Add(std::move(item));
+ }
+ items.Sort(SortByLabel, SortOrderAscending);
+ installableItems.Sort(SortByLabel, SortOrderAscending);
+
+ items.Append(installableItems);
+
+ dialog->SetItems(items);
+
+ dialog->Open();
+
+ // If the "Get More" button has been pressed, show a list of installable addons
+ if (dialog->IsConfirmed())
+ {
+ int selectedIndex = dialog->GetSelectedItem();
+
+ if (0 <= selectedIndex && selectedIndex < items.Size())
+ {
+ gameClient = items[selectedIndex]->GetPath();
+
+ CLog::Log(LOGDEBUG, "Select game client dialog: User selected emulator {}", gameClient);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Select game client dialog: User selected invalid emulator {}",
+ selectedIndex);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Select game client dialog: User cancelled game client installation");
+ }
+ }
+
+ return gameClient;
+}
+
+CGUIDialogSelect* CGUIDialogSelectGameClient::GetDialog(const std::string& title)
+{
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (dialog != nullptr)
+ {
+ dialog->Reset();
+ dialog->SetHeading(CVariant{title});
+ dialog->SetUseDetails(true);
+ }
+
+ return dialog;
+}
+
+void CGUIDialogSelectGameClient::LogGameClients(const GameClientVector& candidates,
+ const GameClientVector& installable)
+{
+ CLog::Log(LOGDEBUG, "Select game client dialog: Found {} candidates",
+ static_cast<unsigned int>(candidates.size()));
+ for (const auto& gameClient : candidates)
+ CLog::Log(LOGDEBUG, "Adding {} as a candidate", gameClient->ID());
+
+ if (!installable.empty())
+ {
+ CLog::Log(LOGDEBUG, "Select game client dialog: Found {} installable clients",
+ static_cast<unsigned int>(installable.size()));
+ for (const auto& gameClient : installable)
+ CLog::Log(LOGDEBUG, "Adding {} as an installable client", gameClient->ID());
+ }
+}
diff --git a/xbmc/games/dialogs/GUIDialogSelectGameClient.h b/xbmc/games/dialogs/GUIDialogSelectGameClient.h
new file mode 100644
index 0000000..c492481
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectGameClient.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016-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 "games/GameTypes.h"
+
+#include <string>
+
+class CGUIDialogSelect;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogSelectGameClient
+{
+public:
+ /*!
+ * \brief Show a series of dialogs that results in a game client being
+ * selected
+ *
+ * \param gamePath The path of the file being played
+ * \param candidates A list of installed candidates that the user can
+ * select from
+ * \param installable A list of installable candidates that the user can
+ * select from
+ *
+ * \return The ID of the selected game client, or empty if no game client
+ * was selected
+ */
+ static std::string ShowAndGetGameClient(const std::string& gamePath,
+ const GameClientVector& candidates,
+ const GameClientVector& installable);
+
+private:
+ /*!
+ * \brief Get an initialized select dialog
+ *
+ * \param title The title of the select dialog
+ *
+ * \return A select dialog with its properties initialized, or nullptr if
+ * the dialog isn't found
+ */
+ static CGUIDialogSelect* GetDialog(const std::string& title);
+
+ /*!
+ * \brief Log the candidates and installable game clients
+ *
+ * Other than logging, this has no side effects.
+ */
+ static void LogGameClients(const GameClientVector& candidates,
+ const GameClientVector& installable);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/GUIDialogSelectSavestate.cpp b/xbmc/games/dialogs/GUIDialogSelectSavestate.cpp
new file mode 100644
index 0000000..d56b1f4
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectSavestate.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020-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 "GUIDialogSelectSavestate.h"
+
+#include "ServiceBroker.h"
+#include "games/dialogs/osd/DialogGameSaves.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGUIDialogSelectSavestate::ShowAndGetSavestate(const std::string& gamePath,
+ std::string& savestatePath)
+{
+ savestatePath = "";
+
+ // Can't ask the user if there's no dialog
+ CDialogGameSaves* dialog = GetDialog();
+ if (dialog == nullptr)
+ return true;
+
+ if (!dialog->Open(gamePath))
+ return true;
+
+ if (dialog->IsConfirmed())
+ {
+ savestatePath = dialog->GetSelectedItemPath();
+ return true;
+ }
+ else if (dialog->IsNewPressed())
+ {
+ CLog::Log(LOGDEBUG, "Select savestate dialog: New savestate selected");
+ return true;
+ }
+
+ // User canceled the dialog
+ return false;
+}
+
+CDialogGameSaves* CGUIDialogSelectSavestate::GetDialog()
+{
+ CDialogGameSaves* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CDialogGameSaves>(
+ WINDOW_DIALOG_GAME_SAVES);
+
+ if (dialog != nullptr)
+ dialog->Reset();
+
+ return dialog;
+}
diff --git a/xbmc/games/dialogs/GUIDialogSelectSavestate.h b/xbmc/games/dialogs/GUIDialogSelectSavestate.h
new file mode 100644
index 0000000..e554142
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectSavestate.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020-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>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameSaves;
+
+class CGUIDialogSelectSavestate
+{
+public:
+ static bool ShowAndGetSavestate(const std::string& gamePath, std::string& savestatePath);
+
+private:
+ static CDialogGameSaves* GetDialog();
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/CMakeLists.txt b/xbmc/games/dialogs/osd/CMakeLists.txt
new file mode 100644
index 0000000..3f7f5bd
--- /dev/null
+++ b/xbmc/games/dialogs/osd/CMakeLists.txt
@@ -0,0 +1,25 @@
+set(SOURCES DialogGameAdvancedSettings.cpp
+ DialogGameOSD.cpp
+ DialogGameOSDHelp.cpp
+ DialogGameSaves.cpp
+ DialogGameStretchMode.cpp
+ DialogGameVideoFilter.cpp
+ DialogGameVideoRotation.cpp
+ DialogGameVideoSelect.cpp
+ DialogGameVolume.cpp
+ DialogInGameSaves.cpp
+)
+
+set(HEADERS DialogGameAdvancedSettings.h
+ DialogGameOSD.h
+ DialogGameOSDHelp.h
+ DialogGameSaves.h
+ DialogGameStretchMode.h
+ DialogGameVideoFilter.h
+ DialogGameVideoRotation.h
+ DialogGameVideoSelect.h
+ DialogGameVolume.h
+ DialogInGameSaves.h
+)
+
+core_add_library(gameosddialogs)
diff --git a/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp
new file mode 100644
index 0000000..83b7a3f
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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 "DialogGameAdvancedSettings.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameAdvancedSettings::CDialogGameAdvancedSettings()
+ : CGUIDialog(WINDOW_DIALOG_GAME_ADVANCED_SETTINGS, "")
+{
+}
+
+bool CDialogGameAdvancedSettings::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ auto gameSettingsHandle = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+ if (gameSettingsHandle)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(gameSettingsHandle->GameClientID(), addon,
+ ADDON::AddonType::GAMEDLL,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ gameSettingsHandle.reset();
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ }
+ }
+
+ return false;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h
new file mode 100644
index 0000000..00b669b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 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 "guilib/GUIDialog.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameAdvancedSettings : public CGUIDialog
+{
+public:
+ CDialogGameAdvancedSettings();
+ ~CDialogGameAdvancedSettings() override = default;
+
+ // implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameOSD.cpp b/xbmc/games/dialogs/osd/DialogGameOSD.cpp
new file mode 100644
index 0000000..3a0eb52
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSD.cpp
@@ -0,0 +1,81 @@
+/*
+ * 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 "DialogGameOSD.h"
+
+#include "DialogGameOSDHelp.h"
+#include "ServiceBroker.h"
+#include "games/GameServices.h"
+#include "games/GameSettings.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameOSD::CDialogGameOSD()
+ : CGUIDialog(WINDOW_DIALOG_GAME_OSD, "GameOSD.xml"), m_helpDialog(new CDialogGameOSDHelp(*this))
+{
+ // Initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CDialogGameOSD::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PARENT_DIR:
+ case ACTION_PREVIOUS_MENU:
+ case ACTION_NAV_BACK:
+ case ACTION_SHOW_OSD:
+ case ACTION_PLAYER_PLAY:
+ {
+ // Disable OSD help if visible
+ if (m_helpDialog->IsVisible() && CServiceBroker::IsServiceManagerUp())
+ {
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ if (gameSettings.ShowOSDHelp())
+ {
+ gameSettings.SetShowOSDHelp(false);
+ return true;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+void CDialogGameOSD::OnInitWindow()
+{
+ // Init parent class
+ CGUIDialog::OnInitWindow();
+
+ // Init help dialog
+ m_helpDialog->OnInitWindow();
+}
+
+void CDialogGameOSD::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ if (CServiceBroker::IsServiceManagerUp())
+ {
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ gameSettings.SetShowOSDHelp(false);
+ }
+}
+
+bool CDialogGameOSD::PlayInBackground(int dialogId)
+{
+ return dialogId == WINDOW_DIALOG_GAME_VOLUME;
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameOSD.h b/xbmc/games/dialogs/osd/DialogGameOSD.h
new file mode 100644
index 0000000..3a8127b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSD.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameOSDHelp;
+
+class CDialogGameOSD : public CGUIDialog
+{
+public:
+ CDialogGameOSD();
+
+ ~CDialogGameOSD() override = default;
+
+ // Implementation of CGUIControl via CGUIDialog
+ bool OnAction(const CAction& action) override;
+
+ // Implementation of CGUIWindow via CGUIDialog
+ void OnDeinitWindow(int nextWindowID) override;
+
+ /*!
+ * \brief Decide if the game should play behind the given dialog
+ *
+ * If true, the game should be played at regular speed.
+ *
+ * \param dialog The current dialog
+ *
+ * \return True if the game should be played at regular speed behind the
+ * dialog, false otherwise
+ */
+ static bool PlayInBackground(int dialogId);
+
+protected:
+ // Implementation of CGUIWindow via CGUIDialog
+ void OnInitWindow() override;
+
+private:
+ std::unique_ptr<CDialogGameOSDHelp> m_helpDialog;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp b/xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp
new file mode 100644
index 0000000..3152172
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 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 "DialogGameOSDHelp.h"
+
+#include "DialogGameOSD.h"
+#include "ServiceBroker.h"
+#include "games/GameServices.h"
+#include "games/controllers/guicontrols/GUIGameController.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const int CDialogGameOSDHelp::CONTROL_ID_HELP_TEXT = 1101;
+const int CDialogGameOSDHelp::CONTROL_ID_GAME_CONTROLLER = 1102;
+
+CDialogGameOSDHelp::CDialogGameOSDHelp(CDialogGameOSD& dialog) : m_dialog(dialog)
+{
+}
+
+void CDialogGameOSDHelp::OnInitWindow()
+{
+ // Set help text
+ //! @todo Define Select + X combo elsewhere
+ // "Press {0:s} to open the menu."
+ std::string helpText = StringUtils::Format(g_localizeStrings.Get(35235), "Select + X");
+
+ CGUIMessage msg(GUI_MSG_LABEL_SET, WINDOW_DIALOG_GAME_OSD, CONTROL_ID_HELP_TEXT);
+ msg.SetLabel(helpText);
+ m_dialog.OnMessage(msg);
+
+ // Set controller
+ if (CServiceBroker::IsServiceManagerUp())
+ {
+ CGameServices& gameServices = CServiceBroker::GetGameServices();
+
+ //! @todo Define SNES controller elsewhere
+ ControllerPtr controller = gameServices.GetController("game.controller.snes");
+ if (controller)
+ {
+ //! @todo Activate controller for all game controller controls
+ CGUIGameController* guiController =
+ dynamic_cast<CGUIGameController*>(m_dialog.GetControl(CONTROL_ID_GAME_CONTROLLER));
+ if (guiController != nullptr)
+ guiController->ActivateController(controller);
+ }
+ }
+}
+
+bool CDialogGameOSDHelp::IsVisible()
+{
+ return IsVisible(CONTROL_ID_HELP_TEXT) || IsVisible(CONTROL_ID_GAME_CONTROLLER);
+}
+
+bool CDialogGameOSDHelp::IsVisible(int windowId)
+{
+ CGUIControl* control = m_dialog.GetControl(windowId);
+ if (control != nullptr)
+ return control->IsVisible();
+
+ return false;
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameOSDHelp.h b/xbmc/games/dialogs/osd/DialogGameOSDHelp.h
new file mode 100644
index 0000000..6153075
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSDHelp.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 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
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameOSD;
+
+class CDialogGameOSDHelp
+{
+public:
+ CDialogGameOSDHelp(CDialogGameOSD& dialog);
+
+ // Initialize help controls
+ void OnInitWindow();
+
+ // Check if any help controls are visible
+ bool IsVisible();
+
+private:
+ // Utility functions
+ bool IsVisible(int windowId);
+
+ // Construction parameters
+ CDialogGameOSD& m_dialog;
+
+ // Help control IDs
+ static const int CONTROL_ID_HELP_TEXT;
+ static const int CONTROL_ID_GAME_CONTROLLER;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameSaves.cpp b/xbmc/games/dialogs/osd/DialogGameSaves.cpp
new file mode 100644
index 0000000..90e7785
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameSaves.cpp
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2020-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 "DialogGameSaves.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogOK.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/addons/GameClient.h"
+#include "games/dialogs/DialogGameDefines.h"
+#include "guilib/GUIBaseContainer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/Key.h"
+#include "utils/FileUtils.h"
+#include "utils/Variant.h"
+#include "view/GUIViewControl.h"
+#include "view/ViewState.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameSaves::CDialogGameSaves()
+ : CGUIDialog(WINDOW_DIALOG_GAME_SAVES, "DialogSelect.xml"),
+ m_viewControl(std::make_unique<CGUIViewControl>()),
+ m_vecList(std::make_unique<CFileItemList>())
+{
+}
+
+bool CDialogGameSaves::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ const int actionId = message.GetParam1();
+
+ switch (actionId)
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ int selectedId = m_viewControl->GetSelectedItem();
+ if (0 <= selectedId && selectedId < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedId);
+ if (item)
+ {
+ for (int i = 0; i < m_vecList->Size(); i++)
+ m_vecList->Get(i)->Select(false);
+
+ item->Select(true);
+
+ OnSelect(*item);
+
+ return true;
+ }
+ }
+ break;
+ }
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ {
+ OnContextMenu(*item);
+ return true;
+ }
+ }
+ break;
+ }
+ case ACTION_RENAME_ITEM:
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ {
+ OnRename(*item);
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ case ACTION_DELETE_ITEM:
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ {
+ OnDelete(*item);
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ const int controlId = message.GetSenderId();
+ switch (controlId)
+ {
+ case CONTROL_SAVES_NEW_BUTTON:
+ {
+ m_bNewPressed = true;
+ Close();
+ break;
+ }
+ case CONTROL_SAVES_CANCEL_BUTTON:
+ {
+ m_selectedItem.reset();
+ m_vecList->Clear();
+ m_bConfirmed = false;
+ Close();
+ break;
+ }
+ default:
+ break;
+ }
+
+ break;
+ }
+
+ case GUI_MSG_SETFOCUS:
+ {
+ const int controlId = message.GetControlId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (m_vecList->IsEmpty())
+ {
+ SET_CONTROL_FOCUS(CONTROL_SAVES_NEW_BUTTON, 0);
+ return true;
+ }
+
+ if (m_viewControl->GetCurrentControl() != controlId)
+ {
+ m_viewControl->SetFocused();
+ return true;
+ }
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CDialogGameSaves::FrameMove()
+{
+ CGUIControl* itemContainer = GetControl(CONTROL_SAVES_DETAILED_LIST);
+ if (itemContainer != nullptr)
+ {
+ if (itemContainer->HasFocus())
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ OnFocus(*item);
+ }
+ }
+ else
+ {
+ OnFocusLost();
+ }
+ }
+
+ CGUIDialog::FrameMove();
+}
+
+void CDialogGameSaves::OnInitWindow()
+{
+ m_viewControl->SetItems(*m_vecList);
+ m_viewControl->SetCurrentView(CONTROL_SAVES_DETAILED_LIST);
+
+ CGUIDialog::OnInitWindow();
+
+ // Select the first item
+ m_viewControl->SetSelectedItem(0);
+
+ // There's a race condition where the item's focus sends the update message
+ // before the window is fully initialized, so explicitly set the info now.
+ if (!m_vecList->IsEmpty())
+ {
+ CFileItemPtr item = m_vecList->Get(0);
+ if (item)
+ {
+ const std::string gameClientId = item->GetProperty(SAVESTATE_GAME_CLIENT).asString();
+ if (!gameClientId.empty())
+ {
+ std::string emulatorName;
+ std::string emulatorIcon;
+
+ using namespace ADDON;
+
+ AddonPtr addon;
+ CAddonMgr& addonManager = CServiceBroker::GetAddonMgr();
+ if (addonManager.GetAddon(m_currentGameClient, addon, OnlyEnabled::CHOICE_NO))
+ {
+ std::shared_ptr<CGameClient> gameClient = std::dynamic_pointer_cast<CGameClient>(addon);
+ if (gameClient)
+ {
+ m_currentGameClient = gameClient->ID();
+
+ emulatorName = gameClient->GetEmulatorName();
+ emulatorIcon = gameClient->Icon();
+ }
+ }
+
+ if (!emulatorName.empty())
+ {
+ CGUIMessage message(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_EMULATOR_NAME);
+ message.SetLabel(emulatorName);
+ OnMessage(message);
+ }
+ if (!emulatorIcon.empty())
+ {
+ CGUIMessage message(GUI_MSG_SET_FILENAME, GetID(), CONTROL_SAVES_EMULATOR_ICON);
+ message.SetLabel(emulatorIcon);
+ OnMessage(message);
+ }
+ }
+
+ const std::string caption = item->GetProperty(SAVESTATE_CAPTION).asString();
+ if (!caption.empty())
+ {
+ m_currentCaption = caption;
+
+ CGUIMessage message(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_DESCRIPTION);
+ message.SetLabel(m_currentCaption);
+ OnMessage(message);
+ }
+ }
+ }
+}
+
+void CDialogGameSaves::OnDeinitWindow(int nextWindowID)
+{
+ m_viewControl->Clear();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ // Get selected item
+ for (int i = 0; i < m_vecList->Size(); ++i)
+ {
+ CFileItemPtr item = m_vecList->Get(i);
+ if (item->IsSelected())
+ {
+ m_selectedItem = item;
+ break;
+ }
+ }
+
+ m_vecList->Clear();
+}
+
+void CDialogGameSaves::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewControl->Reset();
+ m_viewControl->SetParentWindow(GetID());
+ m_viewControl->AddView(GetControl(CONTROL_SAVES_DETAILED_LIST));
+}
+
+void CDialogGameSaves::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewControl->Reset();
+}
+
+void CDialogGameSaves::Reset()
+{
+ m_bConfirmed = false;
+ m_bNewPressed = false;
+
+ m_vecList->Clear();
+ m_selectedItem.reset();
+}
+
+bool CDialogGameSaves::Open(const std::string& gamePath)
+{
+ CFileItemList items;
+
+ RETRO::CSavestateDatabase db;
+ if (!db.GetSavestatesNav(items, gamePath))
+ return false;
+
+ if (items.IsEmpty())
+ return false;
+
+ items.Sort(SortByDate, SortOrderDescending);
+
+ SetItems(items);
+
+ CGUIDialog::Open();
+
+ return true;
+}
+
+std::string CDialogGameSaves::GetSelectedItemPath()
+{
+ if (m_selectedItem)
+ return m_selectedItem->GetPath();
+
+ return "";
+}
+
+void CDialogGameSaves::SetItems(const CFileItemList& itemList)
+{
+ m_vecList->Clear();
+
+ // Need to make internal copy of list to be sure dialog is owner of it
+ m_vecList->Copy(itemList);
+
+ m_viewControl->SetItems(*m_vecList);
+}
+
+void CDialogGameSaves::OnSelect(const CFileItem& item)
+{
+ m_bConfirmed = true;
+ Close();
+}
+
+void CDialogGameSaves::OnFocus(const CFileItem& item)
+{
+ const std::string caption = item.GetProperty(SAVESTATE_CAPTION).asString();
+ const std::string gameClientId = item.GetProperty(SAVESTATE_GAME_CLIENT).asString();
+
+ HandleCaption(caption);
+ HandleGameClient(gameClientId);
+}
+
+void CDialogGameSaves::OnFocusLost()
+{
+ HandleCaption("");
+ HandleGameClient("");
+}
+
+void CDialogGameSaves::OnContextMenu(CFileItem& item)
+{
+ CContextButtons buttons;
+
+ buttons.Add(0, 118); // "Rename"
+ buttons.Add(1, 117); // "Delete"
+
+ const int index = CGUIDialogContextMenu::Show(buttons);
+
+ if (index == 0)
+ OnRename(item);
+ else if (index == 1)
+ OnDelete(item);
+}
+
+void CDialogGameSaves::OnRename(CFileItem& item)
+{
+ const std::string& savestatePath = item.GetPath();
+
+ // Get savestate properties
+ RETRO::CSavestateDatabase db;
+ std::unique_ptr<RETRO::ISavestate> savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+ db.GetSavestate(savestatePath, *savestate);
+
+ std::string label(savestate->Label());
+
+ // "Enter new filename"
+ if (CGUIKeyboardFactory::ShowAndGetInput(label, CVariant{g_localizeStrings.Get(16013)}, true) &&
+ label != savestate->Label())
+ {
+ std::unique_ptr<RETRO::ISavestate> newSavestate = db.RenameSavestate(savestatePath, label);
+ if (newSavestate)
+ {
+ RETRO::CSavestateDatabase::GetSavestateItem(*newSavestate, savestatePath, item);
+
+ // Refresh thumbnails
+ m_viewControl->SetItems(*m_vecList);
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+void CDialogGameSaves::OnDelete(CFileItem& item)
+{
+ // "Confirm delete"
+ // "Would you like to delete the selected file(s)?[CR]Warning - this action can't be undone!"
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, CVariant{125}))
+ {
+ const std::string& savestatePath = item.GetPath();
+
+ RETRO::CSavestateDatabase db;
+ if (db.DeleteSavestate(savestatePath))
+ {
+ m_vecList->Remove(&item);
+
+ // Refresh thumbnails
+ m_viewControl->SetItems(*m_vecList);
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+void CDialogGameSaves::HandleCaption(const std::string& caption)
+{
+ if (caption != m_currentCaption)
+ {
+ m_currentCaption = caption;
+
+ // Update the GUI label
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_DESCRIPTION);
+ msg.SetLabel(m_currentCaption);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, GetID());
+ }
+}
+
+void CDialogGameSaves::HandleGameClient(const std::string& gameClientId)
+{
+ if (gameClientId == m_currentGameClient)
+ return;
+
+ m_currentGameClient = gameClientId;
+ if (m_currentGameClient.empty())
+ return;
+
+ // Get game client properties
+ std::shared_ptr<CGameClient> gameClient;
+ std::string emulatorName;
+ std::string iconPath;
+
+ using namespace ADDON;
+
+ AddonPtr addon;
+ CAddonMgr& addonManager = CServiceBroker::GetAddonMgr();
+ if (addonManager.GetAddon(m_currentGameClient, addon, OnlyEnabled::CHOICE_NO))
+ gameClient = std::dynamic_pointer_cast<CGameClient>(addon);
+
+ if (gameClient)
+ {
+ emulatorName = gameClient->GetEmulatorName();
+ iconPath = gameClient->Icon();
+ }
+
+ // Update the GUI elements
+ if (!emulatorName.empty())
+ {
+ CGUIMessage message(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_EMULATOR_NAME);
+ message.SetLabel(emulatorName);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+ }
+ if (!iconPath.empty())
+ {
+ CGUIMessage message(GUI_MSG_SET_FILENAME, GetID(), CONTROL_SAVES_EMULATOR_ICON);
+ message.SetLabel(iconPath);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+ }
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameSaves.h b/xbmc/games/dialogs/osd/DialogGameSaves.h
new file mode 100644
index 0000000..3605ee6
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameSaves.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020-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 "guilib/GUIDialog.h"
+
+#include <memory>
+#include <string>
+
+class CFileItem;
+class CFileItemList;
+class CGUIMessage;
+class CGUIViewControl;
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameSaves : public CGUIDialog
+{
+public:
+ CDialogGameSaves();
+ ~CDialogGameSaves() override = default;
+
+ // implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of CGUIWindow via CGUIDialog
+ void FrameMove() override;
+
+ // Player interface
+ void Reset();
+ bool Open(const std::string& gamePath);
+ bool IsConfirmed() const { return m_bConfirmed; }
+ bool IsNewPressed() const { return m_bNewPressed; }
+ std::string GetSelectedItemPath();
+
+protected:
+ // implementation of CGUIWIndow via CGUIDialog
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+
+private:
+ using CGUIControl::OnFocus;
+
+ /*!
+ * \breif Called when opening to set the item list
+ */
+ void SetItems(const CFileItemList& itemList);
+
+ /*!
+ * \brief Called when an item has been selected
+ */
+ void OnSelect(const CFileItem& item);
+
+ /*!
+ * \brief Called every frame with the item being focused
+ */
+ void OnFocus(const CFileItem& item);
+
+ /*!
+ * \brief Called every frame if no item is focused
+ */
+ void OnFocusLost();
+
+ /*!
+ * \brief Called when a context menu is opened for an item
+ */
+ void OnContextMenu(CFileItem& item);
+
+ /*!
+ * \brief Called when "Rename" is selected from the context menu
+ */
+ void OnRename(CFileItem& item);
+
+ /*!
+ * \brief Called when "Delete" is selected from the context menu
+ */
+ void OnDelete(CFileItem& item);
+
+ /*!
+ * \brief Called every frame with the caption to set
+ */
+ void HandleCaption(const std::string& caption);
+
+ /*!
+ * \brief Called every frame with the game client to set
+ */
+ void HandleGameClient(const std::string& gameClientId);
+
+ // Dialog parameters
+ std::unique_ptr<CGUIViewControl> m_viewControl;
+ std::unique_ptr<CFileItemList> m_vecList;
+ std::shared_ptr<CFileItem> m_selectedItem;
+
+ // Player parameters
+ bool m_bConfirmed{false};
+ bool m_bNewPressed{false};
+
+ // State parameters
+ std::string m_currentCaption;
+ std::string m_currentGameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameStretchMode.cpp b/xbmc/games/dialogs/osd/DialogGameStretchMode.cpp
new file mode 100644
index 0000000..98c1e07
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameStretchMode.cpp
@@ -0,0 +1,128 @@
+/*
+ * 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 "DialogGameStretchMode.h"
+
+#include "FileItem.h"
+#include "cores/RetroPlayer/RetroPlayerUtils.h"
+#include "cores/RetroPlayer/guibridge/GUIGameVideoHandle.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/Variant.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const std::vector<CDialogGameStretchMode::StretchModeProperties>
+ CDialogGameStretchMode::m_allStretchModes = {
+ {630, RETRO::STRETCHMODE::Normal},
+ // { 631, RETRO::STRETCHMODE::Zoom }, //! @todo RetroArch allows trimming some outer
+ // pixels
+ {632, RETRO::STRETCHMODE::Stretch4x3},
+ {35232, RETRO::STRETCHMODE::Fullscreen},
+ {635, RETRO::STRETCHMODE::Original},
+};
+
+CDialogGameStretchMode::CDialogGameStretchMode()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_GAME_STRETCH_MODE)
+{
+}
+
+std::string CDialogGameStretchMode::GetHeading()
+{
+ return g_localizeStrings.Get(35233); // "Stretch mode"
+}
+
+void CDialogGameStretchMode::PreInit()
+{
+ m_stretchModes.clear();
+
+ for (const auto& stretchMode : m_allStretchModes)
+ {
+ bool bSupported = false;
+
+ switch (stretchMode.stretchMode)
+ {
+ case RETRO::STRETCHMODE::Normal:
+ case RETRO::STRETCHMODE::Original:
+ bSupported = true;
+ break;
+
+ case RETRO::STRETCHMODE::Stretch4x3:
+ case RETRO::STRETCHMODE::Fullscreen:
+ if (m_gameVideoHandle)
+ {
+ bSupported = m_gameVideoHandle->SupportsRenderFeature(RETRO::RENDERFEATURE::STRETCH) ||
+ m_gameVideoHandle->SupportsRenderFeature(RETRO::RENDERFEATURE::PIXEL_RATIO);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (bSupported)
+ m_stretchModes.emplace_back(stretchMode);
+ }
+}
+
+void CDialogGameStretchMode::GetItems(CFileItemList& items)
+{
+ for (const auto& stretchMode : m_stretchModes)
+ {
+ CFileItemPtr item = std::make_shared<CFileItem>(g_localizeStrings.Get(stretchMode.stringIndex));
+
+ const std::string stretchModeId =
+ RETRO::CRetroPlayerUtils::StretchModeToIdentifier(stretchMode.stretchMode);
+ if (!stretchModeId.empty())
+ item->SetProperty("game.stretchmode", CVariant{stretchModeId});
+ items.Add(std::move(item));
+ }
+}
+
+void CDialogGameStretchMode::OnItemFocus(unsigned int index)
+{
+ if (index < m_stretchModes.size())
+ {
+ const RETRO::STRETCHMODE stretchMode = m_stretchModes[index].stretchMode;
+
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+ if (gameSettings.StretchMode() != stretchMode)
+ {
+ gameSettings.SetStretchMode(stretchMode);
+ gameSettings.NotifyObservers(ObservableMessageSettingsChanged);
+ }
+ }
+}
+
+unsigned int CDialogGameStretchMode::GetFocusedItem() const
+{
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ for (unsigned int i = 0; i < m_stretchModes.size(); i++)
+ {
+ const RETRO::STRETCHMODE stretchMode = m_stretchModes[i].stretchMode;
+ if (stretchMode == gameSettings.StretchMode())
+ return i;
+ }
+
+ return 0;
+}
+
+void CDialogGameStretchMode::PostExit()
+{
+ m_stretchModes.clear();
+}
+
+bool CDialogGameStretchMode::OnClickAction()
+{
+ Close();
+ return true;
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameStretchMode.h b/xbmc/games/dialogs/osd/DialogGameStretchMode.h
new file mode 100644
index 0000000..1e05061
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameStretchMode.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "DialogGameVideoSelect.h"
+#include "cores/GameSettings.h"
+
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameStretchMode : public CDialogGameVideoSelect
+{
+public:
+ CDialogGameStretchMode();
+ ~CDialogGameStretchMode() override = default;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+
+private:
+ struct StretchModeProperties
+ {
+ int stringIndex;
+ RETRO::STRETCHMODE stretchMode;
+ };
+
+ std::vector<StretchModeProperties> m_stretchModes;
+
+ /*!
+ * \brief The list of all the stretch modes along with their properties
+ */
+ static const std::vector<StretchModeProperties> m_allStretchModes;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp b/xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp
new file mode 100644
index 0000000..a5bba0b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp
@@ -0,0 +1,157 @@
+/*
+ * 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 "DialogGameVideoFilter.h"
+
+#include "cores/RetroPlayer/guibridge/GUIGameVideoHandle.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace KODI;
+using namespace GAME;
+
+namespace
+{
+struct ScalingMethodProperties
+{
+ int nameIndex;
+ int categoryIndex;
+ int descriptionIndex;
+ RETRO::SCALINGMETHOD scalingMethod;
+};
+
+const std::vector<ScalingMethodProperties> scalingMethods = {
+ {16301, 16296, 16298, RETRO::SCALINGMETHOD::NEAREST},
+ {16302, 16297, 16299, RETRO::SCALINGMETHOD::LINEAR},
+};
+} // namespace
+
+CDialogGameVideoFilter::CDialogGameVideoFilter()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_GAME_VIDEO_FILTER)
+{
+}
+
+std::string CDialogGameVideoFilter::GetHeading()
+{
+ return g_localizeStrings.Get(35225); // "Video filter"
+}
+
+void CDialogGameVideoFilter::PreInit()
+{
+ m_items.Clear();
+
+ InitVideoFilters();
+
+ if (m_items.Size() == 0)
+ {
+ CFileItemPtr item = std::make_shared<CFileItem>(g_localizeStrings.Get(231)); // "None"
+ m_items.Add(std::move(item));
+ }
+
+ m_bHasDescription = false;
+}
+
+void CDialogGameVideoFilter::InitVideoFilters()
+{
+ if (m_gameVideoHandle)
+ {
+ for (const auto& scalingMethodProps : scalingMethods)
+ {
+ if (m_gameVideoHandle->SupportsScalingMethod(scalingMethodProps.scalingMethod))
+ {
+ RETRO::CRenderVideoSettings videoSettings;
+ videoSettings.SetScalingMethod(scalingMethodProps.scalingMethod);
+
+ CFileItemPtr item =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(scalingMethodProps.nameIndex));
+ item->SetLabel2(g_localizeStrings.Get(scalingMethodProps.categoryIndex));
+ item->SetProperty("game.videofilter", CVariant{videoSettings.GetVideoFilter()});
+ item->SetProperty("game.videofilterdescription",
+ CVariant{g_localizeStrings.Get(scalingMethodProps.descriptionIndex)});
+ m_items.Add(std::move(item));
+ }
+ }
+ }
+}
+
+void CDialogGameVideoFilter::GetItems(CFileItemList& items)
+{
+ for (const auto& item : m_items)
+ items.Add(item);
+}
+
+void CDialogGameVideoFilter::OnItemFocus(unsigned int index)
+{
+ if (static_cast<int>(index) < m_items.Size())
+ {
+ CFileItemPtr item = m_items[index];
+
+ std::string videoFilter;
+ std::string description;
+ GetProperties(*item, videoFilter, description);
+
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ if (gameSettings.VideoFilter() != videoFilter)
+ {
+ gameSettings.SetVideoFilter(videoFilter);
+ gameSettings.NotifyObservers(ObservableMessageSettingsChanged);
+
+ OnDescriptionChange(description);
+ m_bHasDescription = true;
+ }
+ else if (!m_bHasDescription)
+ {
+ OnDescriptionChange(description);
+ m_bHasDescription = true;
+ }
+ }
+}
+
+unsigned int CDialogGameVideoFilter::GetFocusedItem() const
+{
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ for (int i = 0; i < m_items.Size(); i++)
+ {
+ std::string videoFilter;
+ std::string description;
+ GetProperties(*m_items[i], videoFilter, description);
+
+ if (videoFilter == gameSettings.VideoFilter())
+ {
+ return i;
+ }
+ }
+
+ return 0;
+}
+
+void CDialogGameVideoFilter::PostExit()
+{
+ m_items.Clear();
+}
+
+bool CDialogGameVideoFilter::OnClickAction()
+{
+ Close();
+ return true;
+}
+
+void CDialogGameVideoFilter::GetProperties(const CFileItem& item,
+ std::string& videoFilter,
+ std::string& description)
+{
+ videoFilter = item.GetProperty("game.videofilter").asString();
+ description = item.GetProperty("game.videofilterdescription").asString();
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoFilter.h b/xbmc/games/dialogs/osd/DialogGameVideoFilter.h
new file mode 100644
index 0000000..7ec8c76
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoFilter.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "DialogGameVideoSelect.h"
+#include "FileItem.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameVideoFilter : public CDialogGameVideoSelect
+{
+public:
+ CDialogGameVideoFilter();
+ ~CDialogGameVideoFilter() override = default;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+
+private:
+ void InitVideoFilters();
+
+ static void GetProperties(const CFileItem& item,
+ std::string& videoFilter,
+ std::string& description);
+
+ CFileItemList m_items;
+
+ //! \brief Set to true when a description has first been set
+ bool m_bHasDescription = false;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp b/xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp
new file mode 100644
index 0000000..7037a6b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 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 "DialogGameVideoRotation.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/Variant.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameVideoRotation::CDialogGameVideoRotation()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_GAME_VIDEO_ROTATION)
+{
+}
+
+std::string CDialogGameVideoRotation::GetHeading()
+{
+ return g_localizeStrings.Get(35227); // "Rotation"
+}
+
+void CDialogGameVideoRotation::PreInit()
+{
+ m_rotations.clear();
+
+ // Present the user with clockwise rotation
+ m_rotations.push_back(0);
+ m_rotations.push_back(270);
+ m_rotations.push_back(180);
+ m_rotations.push_back(90);
+}
+
+void CDialogGameVideoRotation::GetItems(CFileItemList& items)
+{
+ for (unsigned int rotation : m_rotations)
+ {
+ CFileItemPtr item = std::make_shared<CFileItem>(GetRotationLabel(rotation));
+ item->SetProperty("game.videorotation", CVariant{rotation});
+ items.Add(std::move(item));
+ }
+}
+
+void CDialogGameVideoRotation::OnItemFocus(unsigned int index)
+{
+ if (index < m_rotations.size())
+ {
+ const unsigned int rotationDegCCW = m_rotations[index];
+
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+ if (gameSettings.RotationDegCCW() != rotationDegCCW)
+ {
+ gameSettings.SetRotationDegCCW(rotationDegCCW);
+ gameSettings.NotifyObservers(ObservableMessageSettingsChanged);
+ }
+ }
+}
+
+unsigned int CDialogGameVideoRotation::GetFocusedItem() const
+{
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ for (unsigned int i = 0; i < m_rotations.size(); i++)
+ {
+ const unsigned int rotationDegCCW = m_rotations[i];
+ if (rotationDegCCW == gameSettings.RotationDegCCW())
+ return i;
+ }
+
+ return 0;
+}
+
+void CDialogGameVideoRotation::PostExit()
+{
+ m_rotations.clear();
+}
+
+bool CDialogGameVideoRotation::OnClickAction()
+{
+ Close();
+ return true;
+}
+
+std::string CDialogGameVideoRotation::GetRotationLabel(unsigned int rotationDegCCW)
+{
+ switch (rotationDegCCW)
+ {
+ case 0:
+ return g_localizeStrings.Get(35228); // 0
+ case 90:
+ return g_localizeStrings.Get(35231); // 270
+ case 180:
+ return g_localizeStrings.Get(35230); // 180
+ case 270:
+ return g_localizeStrings.Get(35229); // 90
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoRotation.h b/xbmc/games/dialogs/osd/DialogGameVideoRotation.h
new file mode 100644
index 0000000..ef454db
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoRotation.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 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 "DialogGameVideoSelect.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameVideoRotation : public CDialogGameVideoSelect
+{
+public:
+ CDialogGameVideoRotation();
+ ~CDialogGameVideoRotation() override = default;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+
+private:
+ // Helper functions
+ static std::string GetRotationLabel(unsigned int rotationDegCCW);
+
+ // Dialog parameters
+ std::vector<unsigned int> m_rotations; // Degrees counter-clockwise
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp b/xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp
new file mode 100644
index 0000000..a69b26f
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp
@@ -0,0 +1,254 @@
+/*
+ * 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 "DialogGameVideoSelect.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameVideoHandle.h"
+#include "games/dialogs/DialogGameDefines.h"
+#include "guilib/GUIBaseContainer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/ActionIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/GUIViewControl.h"
+#include "view/ViewState.h"
+#include "windowing/GraphicContext.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameVideoSelect::CDialogGameVideoSelect(int windowId)
+ : CGUIDialog(windowId, "DialogSelect.xml"),
+ m_viewControl(new CGUIViewControl),
+ m_vecItems(new CFileItemList)
+{
+ // Initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CDialogGameVideoSelect::~CDialogGameVideoSelect() = default;
+
+bool CDialogGameVideoSelect::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ RegisterDialog();
+
+ // Don't init this dialog if we aren't playing a game
+ if (!m_gameVideoHandle || !m_gameVideoHandle->IsPlayingGame())
+ return false;
+
+ break;
+ }
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ UnregisterDialog();
+
+ break;
+ }
+ case GUI_MSG_SETFOCUS:
+ {
+ const int controlId = message.GetControlId();
+ if (m_viewControl->HasControl(controlId) && m_viewControl->GetCurrentControl() != controlId)
+ {
+ m_viewControl->SetFocused();
+ return true;
+ }
+ break;
+ }
+ case GUI_MSG_CLICKED:
+ {
+ const int actionId = message.GetParam1();
+ if (actionId == ACTION_SELECT_ITEM || actionId == ACTION_MOUSE_LEFT_CLICK)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnClickAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_CONTEXT_MENU || actionId == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnMenuAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_CREATE_BOOKMARK)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnOverwriteAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_RENAME_ITEM)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnRenameAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_DELETE_ITEM)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnDeleteAction())
+ return true;
+ }
+ }
+
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ RefreshList();
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CDialogGameVideoSelect::FrameMove()
+{
+ CGUIBaseContainer* thumbs = dynamic_cast<CGUIBaseContainer*>(GetControl(CONTROL_VIDEO_THUMBS));
+ if (thumbs != nullptr)
+ OnItemFocus(thumbs->GetSelectedItem());
+
+ CGUIDialog::FrameMove();
+}
+
+void CDialogGameVideoSelect::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewControl->SetParentWindow(GetID());
+ m_viewControl->AddView(GetControl(CONTROL_VIDEO_THUMBS));
+}
+
+void CDialogGameVideoSelect::OnWindowUnload()
+{
+ m_viewControl->Reset();
+
+ CGUIDialog::OnWindowUnload();
+}
+
+void CDialogGameVideoSelect::OnInitWindow()
+{
+ PreInit();
+
+ CGUIDialog::OnInitWindow();
+
+ Update();
+
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), CONTROL_VIDEO_THUMBS);
+ OnMessage(msg);
+
+ std::string heading = GetHeading();
+ SET_CONTROL_LABEL(CONTROL_VIDEO_HEADING, heading);
+
+ FrameMove();
+}
+
+void CDialogGameVideoSelect::OnDeinitWindow(int nextWindowID)
+{
+ Clear();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ PostExit();
+
+ SaveSettings();
+}
+
+void CDialogGameVideoSelect::Update()
+{
+ //! @todo
+ // Lock our display, as this window is rendered from the player thread
+ // CServiceBroker::GetWinSystem()->GetGfxContext().Lock();
+
+ m_viewControl->SetCurrentView(DEFAULT_VIEW_ICONS);
+
+ // Empty the list ready for population
+ Clear();
+
+ RefreshList();
+
+ // CServiceBroker::GetWinSystem()->GetGfxContext().Unlock();
+}
+
+void CDialogGameVideoSelect::Clear()
+{
+ m_viewControl->Clear();
+ m_vecItems->Clear();
+}
+
+void CDialogGameVideoSelect::RefreshList()
+{
+ m_vecItems->Clear();
+
+ GetItems(*m_vecItems);
+
+ m_viewControl->SetItems(*m_vecItems);
+
+ auto focusedIndex = GetFocusedItem();
+ m_viewControl->SetSelectedItem(focusedIndex);
+ OnItemFocus(focusedIndex);
+
+ // Refresh the panel container
+ CGUIMessage message(GUI_MSG_REFRESH_THUMBS, GetID(), CONTROL_VIDEO_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+}
+
+void CDialogGameVideoSelect::SaveSettings()
+{
+ CGameSettings& defaultSettings = CMediaSettings::GetInstance().GetDefaultGameSettings();
+ CGameSettings& currentSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ if (defaultSettings != currentSettings)
+ {
+ defaultSettings = currentSettings;
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+}
+
+void CDialogGameVideoSelect::OnDescriptionChange(const std::string& description)
+{
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), CONTROL_VIDEO_THUMBS);
+ msg.SetLabel(description);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, GetID());
+}
+
+void CDialogGameVideoSelect::RegisterDialog()
+{
+ m_gameVideoHandle = CServiceBroker::GetGameRenderManager().RegisterDialog(*this);
+}
+
+void CDialogGameVideoSelect::UnregisterDialog()
+{
+ m_gameVideoHandle.reset();
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoSelect.h b/xbmc/games/dialogs/osd/DialogGameVideoSelect.h
new file mode 100644
index 0000000..c14c94e
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoSelect.h
@@ -0,0 +1,84 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <memory>
+
+class CFileItemList;
+class CGUIViewControl;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIGameVideoHandle;
+}
+
+namespace GAME
+{
+class CDialogGameVideoSelect : public CGUIDialog
+{
+public:
+ ~CDialogGameVideoSelect() override;
+
+ // implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of CGUIWindow via CGUIDialog
+ void FrameMove() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+protected:
+ CDialogGameVideoSelect(int windowId);
+
+ // implementation of CGUIWindow via CGUIDialog
+ void OnWindowUnload() override;
+ void OnWindowLoaded() override;
+ void OnInitWindow() override;
+
+ // Video select interface
+ virtual std::string GetHeading() = 0;
+ virtual void PreInit() = 0;
+ virtual void GetItems(CFileItemList& items) = 0;
+ virtual void OnItemFocus(unsigned int index) = 0;
+ virtual unsigned int GetFocusedItem() const = 0;
+ virtual void PostExit() = 0;
+ // override this to do something when an item is selected
+ virtual bool OnClickAction() { return false; }
+ // override this to do something when an item's context menu is opened
+ virtual bool OnMenuAction() { return false; }
+ // override this to do something when an item is overwritten with a new savestate
+ virtual bool OnOverwriteAction() { return false; }
+ // override this to do something when an item is renamed
+ virtual bool OnRenameAction() { return false; }
+ // override this to do something when an item is deleted
+ virtual bool OnDeleteAction() { return false; }
+
+ // GUI functions
+ void RefreshList();
+ void OnDescriptionChange(const std::string& description);
+
+ std::shared_ptr<RETRO::CGUIGameVideoHandle> m_gameVideoHandle;
+
+private:
+ void Update();
+ void Clear();
+
+ void SaveSettings();
+
+ void RegisterDialog();
+ void UnregisterDialog();
+
+ std::unique_ptr<CGUIViewControl> m_viewControl;
+ std::unique_ptr<CFileItemList> m_vecItems;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVolume.cpp b/xbmc/games/dialogs/osd/DialogGameVolume.cpp
new file mode 100644
index 0000000..8467269
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVolume.cpp
@@ -0,0 +1,149 @@
+/*
+ * 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 "DialogGameVolume.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "dialogs/GUIDialogVolumeBar.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIDialog.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUISliderControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "utils/Variant.h"
+
+#include <cmath>
+
+using namespace KODI;
+using namespace GAME;
+
+#define CONTROL_LABEL 12 //! @todo Remove me
+
+CDialogGameVolume::CDialogGameVolume()
+{
+ // Initialize CGUIWindow
+ SetID(WINDOW_DIALOG_GAME_VOLUME);
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CDialogGameVolume::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_STATE_CHANGED:
+ {
+ int controlId = message.GetControlId();
+ if (controlId == GetID())
+ {
+ OnStateChanged();
+ return true;
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialogSlider::OnMessage(message);
+}
+
+void CDialogGameVolume::OnInitWindow()
+{
+ m_volumePercent = m_oldVolumePercent = GetVolumePercent();
+
+ CGUIDialogSlider::OnInitWindow();
+
+ // Set slider parameters
+ SetModalityType(DialogModalityType::MODAL);
+ SetSlider(GetLabel(), GetVolumePercent(), VOLUME_MIN, VOLUME_DELTA, VOLUME_MAX, this, nullptr);
+
+ SET_CONTROL_HIDDEN(CONTROL_LABEL);
+
+ CGUIDialogVolumeBar* dialogVolumeBar = dynamic_cast<CGUIDialogVolumeBar*>(
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_DIALOG_VOLUME_BAR));
+ if (dialogVolumeBar != nullptr)
+ dialogVolumeBar->RegisterCallback(this);
+
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+void CDialogGameVolume::OnDeinitWindow(int nextWindowID)
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+
+ CGUIDialogVolumeBar* dialogVolumeBar = dynamic_cast<CGUIDialogVolumeBar*>(
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_DIALOG_VOLUME_BAR));
+ if (dialogVolumeBar != nullptr)
+ dialogVolumeBar->UnregisterCallback(this);
+
+ CGUIDialogSlider::OnDeinitWindow(nextWindowID);
+}
+
+void CDialogGameVolume::OnSliderChange(void* data, CGUISliderControl* slider)
+{
+ const float volumePercent = slider->GetFloatValue();
+
+ if (std::fabs(volumePercent - m_volumePercent) > 0.1f)
+ {
+ m_volumePercent = volumePercent;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(volumePercent, true);
+ }
+}
+
+bool CDialogGameVolume::IsShown() const
+{
+ return m_active;
+}
+
+void CDialogGameVolume::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (flag == ANNOUNCEMENT::Application && message == "OnVolumeChanged")
+ {
+ const float volumePercent = static_cast<float>(data["volume"].asDouble());
+
+ if (std::fabs(volumePercent - m_volumePercent) > 0.1f)
+ {
+ m_volumePercent = volumePercent;
+
+ CGUIMessage msg(GUI_MSG_STATE_CHANGED, GetID(), GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ }
+}
+
+void CDialogGameVolume::OnStateChanged()
+{
+ if (m_volumePercent != m_oldVolumePercent)
+ {
+ m_oldVolumePercent = m_volumePercent;
+ SetSlider(GetLabel(), m_volumePercent, VOLUME_MIN, VOLUME_DELTA, VOLUME_MAX, this, nullptr);
+ }
+}
+
+float CDialogGameVolume::GetVolumePercent() const
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ return appVolume->GetVolumePercent();
+}
+
+std::string CDialogGameVolume::GetLabel()
+{
+ return g_localizeStrings.Get(13376); // "Volume"
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVolume.h b/xbmc/games/dialogs/osd/DialogGameVolume.h
new file mode 100644
index 0000000..a32339d
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVolume.h
@@ -0,0 +1,80 @@
+/*
+ * 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 "dialogs/GUIDialogSlider.h"
+#include "dialogs/IGUIVolumeBarCallback.h"
+#include "guilib/ISliderCallback.h"
+#include "interfaces/IAnnouncer.h"
+
+#include <set>
+
+namespace KODI
+{
+
+namespace GAME
+{
+class CDialogGameVolume : public CGUIDialogSlider, // GUI interface
+ public ISliderCallback, // GUI callback
+ public IGUIVolumeBarCallback, // Volume bar dialog callback
+ public ANNOUNCEMENT::IAnnouncer // Application callback
+{
+public:
+ CDialogGameVolume();
+ ~CDialogGameVolume() override = default;
+
+ // implementation of CGUIControl via CGUIDialogSlider
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of CGUIWindow via CGUIDialogSlider
+ void OnDeinitWindow(int nextWindowID) override;
+
+ // implementation of ISliderCallback
+ void OnSliderChange(void* data, CGUISliderControl* slider) override;
+
+ // implementation of IGUIVolumeBarCallback
+ bool IsShown() const override;
+
+ // implementation of IAnnouncer
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+protected:
+ // implementation of CGUIWindow via CGUIDialogSlider
+ void OnInitWindow() override;
+
+private:
+ /*!
+ * \brief Call when state change message is received
+ */
+ void OnStateChanged();
+
+ /*!
+ * \brief Get the volume of the first callback
+ *
+ * \return The volume, as a fraction of maximum volume
+ */
+ float GetVolumePercent() const;
+
+ /*!
+ * \brief Get the volume bar label
+ */
+ static std::string GetLabel();
+
+ // Volume parameters
+ const float VOLUME_MIN = 0.0f;
+ const float VOLUME_DELTA = 10.0f;
+ const float VOLUME_MAX = 100.0f;
+ float m_volumePercent = 100.0f;
+ float m_oldVolumePercent = 100.0f;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogInGameSaves.cpp b/xbmc/games/dialogs/osd/DialogInGameSaves.cpp
new file mode 100644
index 0000000..9f66fd5
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogInGameSaves.cpp
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2020-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 "DialogInGameSaves.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "XBDateTime.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "cores/RetroPlayer/guicontrols/GUIGameControl.h"
+#include "cores/RetroPlayer/playback/IPlayback.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogOK.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/dialogs/DialogGameDefines.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace RETRO;
+
+namespace
+{
+CFileItemPtr CreateNewSaveItem()
+{
+ CFileItemPtr item = std::make_shared<CFileItem>(g_localizeStrings.Get(15314)); // "Save"
+
+ // A nonexistent path ensures a gamewindow control won't render any pixels
+ item->SetPath(NO_PIXEL_DATA);
+ item->SetArt("icon", "DefaultAddSource.png");
+ item->SetProperty(SAVESTATE_CAPTION,
+ g_localizeStrings.Get(15315)); // "Save progress to a new save file"
+
+ return item;
+}
+} // namespace
+
+CDialogInGameSaves::CDialogInGameSaves()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_IN_GAME_SAVES), m_newSaveItem(CreateNewSaveItem())
+{
+}
+
+bool CDialogInGameSaves::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_REFRESH_THUMBS:
+ {
+ if (message.GetControlId() == GetID())
+ {
+ const std::string& itemPath = message.GetStringParam();
+ CGUIListItemPtr itemInfo = message.GetItem();
+
+ if (!itemPath.empty())
+ {
+ OnItemRefresh(itemPath, std::move(itemInfo));
+ }
+ else
+ {
+ InitSavedGames();
+ RefreshList();
+ }
+
+ return true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CDialogGameVideoSelect::OnMessage(message);
+}
+
+std::string CDialogInGameSaves::GetHeading()
+{
+ return g_localizeStrings.Get(35249); // "Save / Load"
+}
+
+void CDialogInGameSaves::PreInit()
+{
+ InitSavedGames();
+}
+
+void CDialogInGameSaves::InitSavedGames()
+{
+ m_savestateItems.Clear();
+
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ CSavestateDatabase db;
+ db.GetSavestatesNav(m_savestateItems, gameSettings->GetPlayingGame(),
+ gameSettings->GameClientID());
+
+ m_savestateItems.Sort(SortByDate, SortOrderDescending);
+}
+
+void CDialogInGameSaves::GetItems(CFileItemList& items)
+{
+ items.Add(m_newSaveItem);
+ std::for_each(m_savestateItems.cbegin(), m_savestateItems.cend(),
+ [&items](const auto& item) { items.Add(item); });
+}
+
+void CDialogInGameSaves::OnItemFocus(unsigned int index)
+{
+ if (static_cast<int>(index) < 1 + m_savestateItems.Size())
+ m_focusedItemIndex = index;
+}
+
+unsigned int CDialogInGameSaves::GetFocusedItem() const
+{
+ return m_focusedControl;
+}
+
+void CDialogInGameSaves::OnItemRefresh(const std::string& itemPath, CGUIListItemPtr itemInfo)
+{
+ // Turn the message params into a savestate item
+ CFileItemPtr item = TranslateMessageItem(itemPath, std::move(itemInfo));
+ if (item)
+ {
+ // Look up existing savestate by path
+ auto it =
+ std::find_if(m_savestateItems.cbegin(), m_savestateItems.cend(),
+ [&itemPath](const CFileItemPtr& item) { return item->GetPath() == itemPath; });
+
+ // Update savestate or add a new one
+ if (it != m_savestateItems.cend())
+ **it = std::move(*item);
+ else
+ m_savestateItems.AddFront(std::move(item), 0);
+
+ RefreshList();
+ }
+}
+
+void CDialogInGameSaves::PostExit()
+{
+ m_savestateItems.Clear();
+}
+
+bool CDialogInGameSaves::OnClickAction()
+{
+ if (static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ if (m_focusedItemIndex <= 0)
+ {
+ OnNewSave();
+ return true;
+ }
+ else
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnLoad(*focusedItem);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnMenuAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ CContextButtons buttons;
+
+ buttons.Add(0, 13206); // "Overwrite"
+ buttons.Add(1, 118); // "Rename"
+ buttons.Add(2, 117); // "Delete"
+
+ const int index = CGUIDialogContextMenu::Show(buttons);
+
+ if (index == 0)
+ OnOverwrite(*focusedItem);
+ if (index == 1)
+ OnRename(*focusedItem);
+ else if (index == 2)
+ OnDelete(*focusedItem);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnOverwriteAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnOverwrite(*focusedItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnRenameAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnRename(*focusedItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnDeleteAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnDelete(*focusedItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CDialogInGameSaves::OnNewSave()
+{
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ const std::string savestatePath = gameSettings->CreateSavestate(false);
+ if (savestatePath.empty())
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ return;
+ }
+
+ // Create a simulated savestate to update the GUI faster. We will be notified
+ // of the real savestate info via OnMessage() when the savestate creation
+ // completes.
+ auto savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+
+ savestate->SetType(SAVE_TYPE::MANUAL);
+ savestate->SetCreated(CDateTime::GetUTCDateTime());
+
+ savestate->Finalize();
+
+ CFileItemPtr item = std::make_shared<CFileItem>();
+ CSavestateDatabase::GetSavestateItem(*savestate, savestatePath, *item);
+
+ m_savestateItems.AddFront(std::move(item), 0);
+
+ RefreshList();
+}
+
+void CDialogInGameSaves::OnLoad(CFileItem& focusedItem)
+{
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ // Load savestate
+ if (gameSettings->LoadSavestate(focusedItem.GetPath()))
+ {
+ // Close OSD on successful load
+ gameSettings->CloseOSD();
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+}
+
+void CDialogInGameSaves::OnOverwrite(CFileItem& focusedItem)
+{
+ std::string savestatePath = focusedItem.GetPath();
+ if (savestatePath.empty())
+ return;
+
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ // Update savestate
+ if (gameSettings->UpdateSavestate(savestatePath))
+ {
+ // Create a simulated savestate to update the GUI faster. We will be
+ // notified of the real savestate info via OnMessage() when the
+ // overwriting completes.
+ auto savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+
+ savestate->SetType(SAVE_TYPE::MANUAL);
+ savestate->SetLabel(focusedItem.GetProperty(SAVESTATE_LABEL).asString());
+ savestate->SetCaption(focusedItem.GetProperty(SAVESTATE_CAPTION).asString());
+ savestate->SetCreated(CDateTime::GetUTCDateTime());
+
+ savestate->Finalize();
+
+ CSavestateDatabase::GetSavestateItem(*savestate, savestatePath, focusedItem);
+
+ RefreshList();
+ }
+ else
+ {
+ // Log an error and notify the user
+ CLog::Log(LOGERROR, "Failed to overwrite savestate at {}", CURL::GetRedacted(savestatePath));
+
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+}
+
+void CDialogInGameSaves::OnRename(CFileItem& focusedItem)
+{
+ const std::string& savestatePath = focusedItem.GetPath();
+ if (savestatePath.empty())
+ return;
+
+ RETRO::CSavestateDatabase db;
+
+ std::string label;
+
+ std::unique_ptr<RETRO::ISavestate> savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+ if (db.GetSavestate(savestatePath, *savestate))
+ label = savestate->Label();
+
+ // "Enter new filename"
+ if (CGUIKeyboardFactory::ShowAndGetInput(label, CVariant{g_localizeStrings.Get(16013)}, true) &&
+ label != savestate->Label())
+ {
+ std::unique_ptr<RETRO::ISavestate> newSavestate = db.RenameSavestate(savestatePath, label);
+ if (newSavestate)
+ {
+ RETRO::CSavestateDatabase::GetSavestateItem(*newSavestate, savestatePath, focusedItem);
+
+ RefreshList();
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+void CDialogInGameSaves::OnDelete(CFileItem& focusedItem)
+{
+ // "Confirm delete"
+ // "Would you like to delete the selected file(s)?[CR]Warning - this action can't be undone!"
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, CVariant{125}))
+ {
+ RETRO::CSavestateDatabase db;
+ if (db.DeleteSavestate(focusedItem.GetPath()))
+ {
+ m_savestateItems.Remove(&focusedItem);
+
+ RefreshList();
+
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+ gameSettings->FreeSavestateResources(focusedItem.GetPath());
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+CFileItemPtr CDialogInGameSaves::TranslateMessageItem(const std::string& messagePath,
+ CGUIListItemPtr messageItem)
+{
+ CFileItemPtr item;
+
+ if (messageItem && messageItem->IsFileItem())
+ item = std::static_pointer_cast<CFileItem>(messageItem);
+ else if (messageItem)
+ item = std::make_shared<CFileItem>(*messageItem);
+ else if (!messagePath.empty())
+ {
+ item = std::make_shared<CFileItem>();
+
+ // Load savestate if no item info was given
+ auto savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+ RETRO::CSavestateDatabase db;
+ if (db.GetSavestate(messagePath, *savestate))
+ RETRO::CSavestateDatabase::GetSavestateItem(*savestate, messagePath, *item);
+ else
+ item.reset();
+ }
+
+ return item;
+}
diff --git a/xbmc/games/dialogs/osd/DialogInGameSaves.h b/xbmc/games/dialogs/osd/DialogInGameSaves.h
new file mode 100644
index 0000000..6f98f94
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogInGameSaves.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020-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 "DialogGameVideoSelect.h"
+#include "FileItem.h"
+#include "guilib/GUIListItem.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogInGameSaves : public CDialogGameVideoSelect
+{
+public:
+ CDialogInGameSaves();
+ ~CDialogInGameSaves() override = default;
+
+ // implementation of CGUIControl via CDialogGameVideoSelect
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+ bool OnMenuAction() override;
+ bool OnOverwriteAction() override;
+ bool OnRenameAction() override;
+ bool OnDeleteAction() override;
+
+ void OnNewSave();
+ void OnLoad(CFileItem& focusedItem);
+ void OnOverwrite(CFileItem& focusedItem);
+ void OnRename(CFileItem& focusedItem);
+ void OnDelete(CFileItem& focusedItem);
+
+private:
+ void InitSavedGames();
+ void OnItemRefresh(const std::string& itemPath, CGUIListItemPtr itemInfo);
+
+ /*!
+ * \brief Translates the GUI list item received in a GUI message into a
+ * CFileItem with savestate properties
+ *
+ * When a savestate is overwritten, we optimistically populate the GUI list
+ * with a simulated savestate for immediate user feedback. Later (about a
+ * quarter second) a message arrives with the real savestate info.
+ *
+ * \param messagePath The savestate path, pass as the message's string param
+ * \param messageItem The savestate info, if known, or empty if unknown
+ *
+ * If messageItem is empty, the savestate will be loaded from disk, which
+ * is potentially expensive.
+ *
+ * \return A savestate item for the GUI, or empty if no savestate information
+ * can be obtained
+ */
+ static CFileItemPtr TranslateMessageItem(const std::string& messagePath,
+ CGUIListItemPtr messageItem);
+
+ CFileItemList m_savestateItems;
+ const CFileItemPtr m_newSaveItem;
+ unsigned int m_focusedItemIndex = false;
+};
+} // namespace GAME
+} // namespace KODI
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
diff --git a/xbmc/games/tags/CMakeLists.txt b/xbmc/games/tags/CMakeLists.txt
new file mode 100644
index 0000000..d43cdbb
--- /dev/null
+++ b/xbmc/games/tags/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES GameInfoTag.cpp)
+
+set(HEADERS GameInfoTag.h)
+
+core_add_library(gametags)
diff --git a/xbmc/games/tags/GameInfoTag.cpp b/xbmc/games/tags/GameInfoTag.cpp
new file mode 100644
index 0000000..8a02c2c
--- /dev/null
+++ b/xbmc/games/tags/GameInfoTag.cpp
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2012-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 "GameInfoTag.h"
+
+#include "utils/Archive.h"
+#include "utils/Variant.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+void CGameInfoTag::Reset()
+{
+ m_bLoaded = false;
+ m_strURL.clear();
+ m_strTitle.clear();
+ m_strPlatform.clear();
+ m_genres.clear();
+ m_strDeveloper.clear();
+ m_strOverview.clear();
+ m_year = 0;
+ m_strID.clear();
+ m_strRegion.clear();
+ m_strPublisher.clear();
+ m_strFormat.clear();
+ m_strCartridgeType.clear();
+ m_strGameClient.clear();
+}
+
+CGameInfoTag& CGameInfoTag::operator=(const CGameInfoTag& tag)
+{
+ if (this != &tag)
+ {
+ m_bLoaded = tag.m_bLoaded;
+ m_strURL = tag.m_strURL;
+ m_strTitle = tag.m_strTitle;
+ m_strPlatform = tag.m_strPlatform;
+ m_genres = tag.m_genres;
+ m_strDeveloper = tag.m_strDeveloper;
+ m_strOverview = tag.m_strOverview;
+ m_year = tag.m_year;
+ m_strID = tag.m_strID;
+ m_strRegion = tag.m_strRegion;
+ m_strPublisher = tag.m_strPublisher;
+ m_strFormat = tag.m_strFormat;
+ m_strCartridgeType = tag.m_strCartridgeType;
+ m_strGameClient = tag.m_strGameClient;
+ }
+ return *this;
+}
+
+bool CGameInfoTag::operator==(const CGameInfoTag& tag) const
+{
+ if (this != &tag)
+ {
+ if (m_bLoaded != tag.m_bLoaded)
+ return false;
+
+ if (m_bLoaded)
+ {
+ if (m_strURL != tag.m_strURL)
+ return false;
+ if (m_strTitle != tag.m_strTitle)
+ return false;
+ if (m_strPlatform != tag.m_strPlatform)
+ return false;
+ if (m_genres != tag.m_genres)
+ return false;
+ if (m_strDeveloper != tag.m_strDeveloper)
+ return false;
+ if (m_strOverview != tag.m_strOverview)
+ return false;
+ if (m_year != tag.m_year)
+ return false;
+ if (m_strID != tag.m_strID)
+ return false;
+ if (m_strRegion != tag.m_strRegion)
+ return false;
+ if (m_strPublisher != tag.m_strPublisher)
+ return false;
+ if (m_strFormat != tag.m_strFormat)
+ return false;
+ if (m_strCartridgeType != tag.m_strCartridgeType)
+ return false;
+ if (m_strGameClient != tag.m_strGameClient)
+ return false;
+ }
+ }
+ return true;
+}
+
+void CGameInfoTag::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_bLoaded;
+ ar << m_strURL;
+ ar << m_strTitle;
+ ar << m_strPlatform;
+ ar << m_genres;
+ ar << m_strDeveloper;
+ ar << m_strOverview;
+ ar << m_year;
+ ar << m_strID;
+ ar << m_strRegion;
+ ar << m_strPublisher;
+ ar << m_strFormat;
+ ar << m_strCartridgeType;
+ ar << m_strGameClient;
+ }
+ else
+ {
+ ar >> m_bLoaded;
+ ar >> m_strURL;
+ ar >> m_strTitle;
+ ar >> m_strPlatform;
+ ar >> m_genres;
+ ar >> m_strDeveloper;
+ ar >> m_strOverview;
+ ar >> m_year;
+ ar >> m_strID;
+ ar >> m_strRegion;
+ ar >> m_strPublisher;
+ ar >> m_strFormat;
+ ar >> m_strCartridgeType;
+ ar >> m_strGameClient;
+ }
+}
+
+void CGameInfoTag::Serialize(CVariant& value) const
+{
+ value["loaded"] = m_bLoaded;
+ value["url"] = m_strURL;
+ value["name"] = m_strTitle;
+ value["platform"] = m_strPlatform;
+ value["genres"] = m_genres;
+ value["developer"] = m_strDeveloper;
+ value["overview"] = m_strOverview;
+ value["year"] = m_year;
+ value["id"] = m_strID;
+ value["region"] = m_strRegion;
+ value["publisher"] = m_strPublisher;
+ value["format"] = m_strFormat;
+ value["cartridgetype"] = m_strCartridgeType;
+ value["gameclient"] = m_strGameClient;
+}
+
+void CGameInfoTag::ToSortable(SortItem& sortable, Field field) const
+{
+ // No database entries for games (...yet)
+}
diff --git a/xbmc/games/tags/GameInfoTag.h b/xbmc/games/tags/GameInfoTag.h
new file mode 100644
index 0000000..00287ad
--- /dev/null
+++ b/xbmc/games/tags/GameInfoTag.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012-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 "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameInfoTag : public IArchivable, public ISerializable, public ISortable
+{
+public:
+ CGameInfoTag() { Reset(); }
+ CGameInfoTag(const CGameInfoTag& tag) { *this = tag; }
+ CGameInfoTag& operator=(const CGameInfoTag& tag);
+ virtual ~CGameInfoTag() = default;
+ void Reset();
+
+ bool operator==(const CGameInfoTag& tag) const;
+ bool operator!=(const CGameInfoTag& tag) const { return !(*this == tag); }
+
+ bool IsLoaded() const { return m_bLoaded; }
+ void SetLoaded(bool bOnOff = true) { m_bLoaded = bOnOff; }
+
+ // File path
+ const std::string& GetURL() const { return m_strURL; }
+ void SetURL(const std::string& strURL) { m_strURL = strURL; }
+
+ // Title
+ const std::string& GetTitle() const { return m_strTitle; }
+ void SetTitle(const std::string& strTitle) { m_strTitle = strTitle; }
+
+ // Platform
+ const std::string& GetPlatform() const { return m_strPlatform; }
+ void SetPlatform(const std::string& strPlatform) { m_strPlatform = strPlatform; }
+
+ // Genres
+ const std::vector<std::string>& GetGenres() const { return m_genres; }
+ void SetGenres(const std::vector<std::string>& genres) { m_genres = genres; }
+
+ // Developer
+ const std::string& GetDeveloper() const { return m_strDeveloper; }
+ void SetDeveloper(const std::string& strDeveloper) { m_strDeveloper = strDeveloper; }
+
+ // Overview
+ const std::string& GetOverview() const { return m_strOverview; }
+ void SetOverview(const std::string& strOverview) { m_strOverview = strOverview; }
+
+ // Year
+ unsigned int GetYear() const { return m_year; }
+ void SetYear(unsigned int year) { m_year = year; }
+
+ // Game Code (ID)
+ const std::string& GetID() const { return m_strID; }
+ void SetID(const std::string& strID) { m_strID = strID; }
+
+ // Region
+ const std::string& GetRegion() const { return m_strRegion; }
+ void SetRegion(const std::string& strRegion) { m_strRegion = strRegion; }
+
+ // Publisher / Licensee
+ const std::string& GetPublisher() const { return m_strPublisher; }
+ void SetPublisher(const std::string& strPublisher) { m_strPublisher = strPublisher; }
+
+ // Format (PAL/NTSC)
+ const std::string& GetFormat() const { return m_strFormat; }
+ void SetFormat(const std::string& strFormat) { m_strFormat = strFormat; }
+
+ // Cartridge Type, e.g. "ROM+MBC5+RAM+BATT" or "CD"
+ const std::string& GetCartridgeType() const { return m_strCartridgeType; }
+ void SetCartridgeType(const std::string& strCartridgeType)
+ {
+ m_strCartridgeType = strCartridgeType;
+ }
+
+ // Game client add-on ID
+ const std::string& GetGameClient() const { return m_strGameClient; }
+ void SetGameClient(const std::string& strGameClient) { m_strGameClient = strGameClient; }
+
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+private:
+ bool m_bLoaded;
+ std::string m_strURL;
+ std::string m_strTitle;
+ std::string m_strPlatform;
+ std::vector<std::string> m_genres;
+ std::string m_strDeveloper;
+ std::string m_strOverview;
+ unsigned int m_year;
+ std::string m_strID;
+ std::string m_strRegion;
+ std::string m_strPublisher;
+ std::string m_strFormat;
+ std::string m_strCartridgeType;
+ std::string m_strGameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/windows/CMakeLists.txt b/xbmc/games/windows/CMakeLists.txt
new file mode 100644
index 0000000..314608c
--- /dev/null
+++ b/xbmc/games/windows/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GUIViewStateWindowGames.cpp
+ GUIWindowGames.cpp)
+
+set(HEADERS GUIViewStateWindowGames.h
+ GUIWindowGames.h)
+
+core_add_library(gameswindows)
diff --git a/xbmc/games/windows/GUIViewStateWindowGames.cpp b/xbmc/games/windows/GUIViewStateWindowGames.cpp
new file mode 100644
index 0000000..ffb9bf0
--- /dev/null
+++ b/xbmc/games/windows/GUIViewStateWindowGames.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012-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 "GUIViewStateWindowGames.h"
+
+#include "FileItem.h"
+#include "games/GameUtils.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/MediaSourceSettings.h"
+#include "utils/StringUtils.h"
+#include "view/ViewState.h"
+#include "view/ViewStateSettings.h"
+#include "windowing/GraphicContext.h" // include before ViewState.h
+
+#include <assert.h>
+#include <set>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIViewStateWindowGames::CGUIViewStateWindowGames(const CFileItemList& items)
+ : CGUIViewState(items)
+{
+ if (items.IsVirtualDirectoryRoot())
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS());
+ AddSortMethod(SortByDriveType, 564, LABEL_MASKS());
+ SetSortMethod(SortByLabel);
+ SetSortOrder(SortOrderAscending);
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+ }
+ else
+ {
+ AddSortMethod(SortByFile, 561,
+ LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty
+ AddSortMethod(SortBySize, 553,
+ LABEL_MASKS("%L", "%I", "%L", "%I")); // Filename, Size | Label, Size
+
+ const CViewState* viewState = CViewStateSettings::GetInstance().Get("games");
+ if (viewState)
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ }
+
+ LoadViewState(items.GetPath(), WINDOW_GAMES);
+}
+
+std::string CGUIViewStateWindowGames::GetLockType()
+{
+ return "games";
+}
+
+std::string CGUIViewStateWindowGames::GetExtensions()
+{
+ using namespace ADDON;
+
+ std::set<std::string> exts = CGameUtils::GetGameExtensions();
+
+ // Ensure .zip appears
+ exts.insert(".zip");
+
+ return StringUtils::Join(exts, "|");
+}
+
+VECSOURCES& CGUIViewStateWindowGames::GetSources()
+{
+ VECSOURCES* pGameSources = CMediaSourceSettings::GetInstance().GetSources("games");
+
+ // Guard against source type not existing
+ if (pGameSources == nullptr)
+ {
+ static VECSOURCES empty;
+ return empty;
+ }
+
+ return *pGameSources;
+}
+
+void CGUIViewStateWindowGames::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_GAMES, CViewStateSettings::GetInstance().Get("games"));
+}
diff --git a/xbmc/games/windows/GUIViewStateWindowGames.h b/xbmc/games/windows/GUIViewStateWindowGames.h
new file mode 100644
index 0000000..90ad821
--- /dev/null
+++ b/xbmc/games/windows/GUIViewStateWindowGames.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012-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 "view/GUIViewState.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIViewStateWindowGames : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateWindowGames(const CFileItemList& items);
+
+ ~CGUIViewStateWindowGames() override = default;
+
+ // implementation of CGUIViewState
+ std::string GetLockType() override;
+ std::string GetExtensions() override;
+ VECSOURCES& GetSources() override;
+
+protected:
+ // implementation of CGUIViewState
+ void SaveViewState() override;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/windows/GUIWindowGames.cpp b/xbmc/games/windows/GUIWindowGames.cpp
new file mode 100644
index 0000000..a74186d
--- /dev/null
+++ b/xbmc/games/windows/GUIWindowGames.cpp
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2012-2020 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 "GUIWindowGames.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "application/Application.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/ActionIDs.h"
+#include "media/MediaLockState.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+
+CGUIWindowGames::CGUIWindowGames() : CGUIMediaWindow(WINDOW_GAMES, "MyGames.xml")
+{
+}
+
+bool CGUIWindowGames::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_rootDir.AllowNonLocalSources(true); //! @todo
+
+ // Is this the first time the window is opened?
+ if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty())
+ message.SetStringParam(CMediaSourceSettings::GetInstance().GetDefaultSource("games"));
+
+ //! @todo
+ m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+
+ break;
+ }
+ case GUI_MSG_CLICKED:
+ {
+ if (OnClickMsg(message.GetSenderId(), message.GetParam1()))
+ return true;
+ break;
+ }
+ default:
+ break;
+ }
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+bool CGUIWindowGames::OnClickMsg(int controlId, int actionId)
+{
+ if (!m_viewControl.HasControl(controlId)) // list/thumb control
+ return false;
+
+ const int iItem = m_viewControl.GetSelectedItem();
+
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ if (!pItem)
+ return false;
+
+ switch (actionId)
+ {
+ case ACTION_DELETE_ITEM:
+ {
+ // Is delete allowed?
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_ALLOWFILEDELETION))
+ {
+ OnDeleteItem(iItem);
+ return true;
+ }
+ break;
+ }
+ case ACTION_PLAYER_PLAY:
+ {
+ if (OnClick(iItem))
+ return true;
+ break;
+ }
+ case ACTION_SHOW_INFO:
+ {
+ if (!m_vecItems->IsPlugin())
+ {
+ if (pItem->HasAddonInfo())
+ {
+ CGUIDialogAddonInfo::ShowForItem(pItem);
+ return true;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+void CGUIWindowGames::SetupShares()
+{
+ CGUIMediaWindow::SetupShares();
+
+ // Don't convert zip files to directories. Otherwise, the files will be
+ // opened and scanned for games with a valid extension. If none are found,
+ // the .zip won't be shown.
+ //
+ // This is a problem for MAME roms, because the files inside the .zip don't
+ // have standard extensions.
+ //
+ m_rootDir.SetFlags(XFILE::DIR_FLAG_NO_FILE_DIRS);
+}
+
+bool CGUIWindowGames::OnClick(int iItem, const std::string& player /* = "" */)
+{
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (item)
+ {
+ // Compensate for DIR_FLAG_NO_FILE_DIRS flag
+ if (URIUtils::IsArchive(item->GetPath()))
+ {
+ bool bIsGame = false;
+
+ // If zip file contains no games, assume it is a game
+ CFileItemList items;
+ if (m_rootDir.GetDirectory(CURL(item->GetPath()), items))
+ {
+ if (items.Size() == 0)
+ bIsGame = true;
+ }
+
+ if (!bIsGame)
+ item->m_bIsFolder = true;
+ }
+
+ if (!item->m_bIsFolder)
+ {
+ PlayGame(*item);
+ return true;
+ }
+ }
+
+ return CGUIMediaWindow::OnClick(iItem, player);
+}
+
+void CGUIWindowGames::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+
+ if (item && !item->GetProperty("pluginreplacecontextitems").asBoolean())
+ {
+ if (m_vecItems->IsVirtualDirectoryRoot() || m_vecItems->IsSourcesPath())
+ {
+ // Context buttons for a sources path, like "Add Source", "Remove Source", etc.
+ CGUIDialogContextMenu::GetContextButtons("games", item, buttons);
+ }
+ else
+ {
+ if (item->IsGame())
+ {
+ buttons.Add(CONTEXT_BUTTON_PLAY_ITEM, 208); // Play
+ }
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) &&
+ !item->IsReadOnly())
+ {
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+ buttons.Add(CONTEXT_BUTTON_RENAME, 118);
+ }
+ }
+ }
+
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowGames::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (item)
+ {
+ if (m_vecItems->IsVirtualDirectoryRoot() || m_vecItems->IsSourcesPath())
+ {
+ if (CGUIDialogContextMenu::OnContextButton("games", item, button))
+ {
+ Update(m_vecItems->GetPath());
+ return true;
+ }
+ }
+ switch (button)
+ {
+ case CONTEXT_BUTTON_PLAY_ITEM:
+ PlayGame(*item);
+ return true;
+ case CONTEXT_BUTTON_INFO:
+ CGUIDialogAddonInfo::ShowForItem(item);
+ return true;
+ case CONTEXT_BUTTON_DELETE:
+ OnDeleteItem(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_RENAME:
+ OnRenameItem(itemNumber);
+ return true;
+ default:
+ break;
+ }
+ }
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowGames::OnAddMediaSource()
+{
+ return CGUIDialogMediaSource::ShowAndAddMediaSource("games");
+}
+
+bool CGUIWindowGames::GetDirectory(const std::string& strDirectory, CFileItemList& items)
+{
+ if (!CGUIMediaWindow::GetDirectory(strDirectory, items))
+ return false;
+
+ // Set label
+ std::string label;
+ if (items.GetLabel().empty())
+ {
+ std::string source;
+ if (m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("games"),
+ &source))
+ label = std::move(source);
+ }
+
+ if (!label.empty())
+ items.SetLabel(label);
+
+ // Set content
+ std::string content;
+ if (items.GetContent().empty())
+ {
+ if (!items.IsVirtualDirectoryRoot() && // Don't set content for root directory
+ !items.IsPlugin()) // Don't set content for plugins
+ {
+ content = "games";
+ }
+ }
+
+ if (!content.empty())
+ items.SetContent(content);
+
+ // Ensure a game info tag is created so that files are recognized as games
+ for (const CFileItemPtr& item : items)
+ {
+ if (!item->m_bIsFolder)
+ item->GetGameInfoTag();
+ }
+
+ return true;
+}
+
+std::string CGUIWindowGames::GetStartFolder(const std::string& dir)
+{
+ // From CGUIWindowPictures::GetStartFolder()
+
+ if (StringUtils::EqualsNoCase(dir, "plugins") || StringUtils::EqualsNoCase(dir, "addons"))
+ {
+ return "addons://sources/game/";
+ }
+
+ SetupShares();
+ VECSOURCES shares;
+ m_rootDir.GetSources(shares);
+ bool bIsSourceName = false;
+ int iIndex = CUtil::GetMatchingSource(dir, shares, bIsSourceName);
+ if (iIndex >= 0)
+ {
+ if (iIndex < static_cast<int>(shares.size()) && shares[iIndex].m_iHasLock == LOCK_STATE_LOCKED)
+ {
+ CFileItem item(shares[iIndex]);
+ if (!g_passwordManager.IsItemUnlocked(&item, "games"))
+ return "";
+ }
+ if (bIsSourceName)
+ return shares[iIndex].strPath;
+ return dir;
+ }
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
+
+void CGUIWindowGames::OnItemInfo(int itemNumber)
+{
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (!item)
+ return;
+
+ if (!m_vecItems->IsPlugin())
+ {
+ if (item->IsPlugin() || item->IsScript())
+ CGUIDialogAddonInfo::ShowForItem(item);
+ }
+
+ //! @todo
+ /*
+ CGUIDialogGameInfo* gameInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogGameInfo>(WINDOW_DIALOG_PICTURE_INFO);
+ if (gameInfo)
+ {
+ gameInfo->SetGame(item);
+ gameInfo->Open();
+ }
+ */
+}
+
+bool CGUIWindowGames::PlayGame(const CFileItem& item)
+{
+ CFileItem itemCopy(item);
+ return g_application.PlayMedia(itemCopy, "", PLAYLIST::TYPE_NONE);
+}
diff --git a/xbmc/games/windows/GUIWindowGames.h b/xbmc/games/windows/GUIWindowGames.h
new file mode 100644
index 0000000..2fbabfa
--- /dev/null
+++ b/xbmc/games/windows/GUIWindowGames.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2012-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 "windows/GUIMediaWindow.h"
+
+class CGUIDialogProgress;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIWindowGames : public CGUIMediaWindow
+{
+public:
+ CGUIWindowGames();
+ ~CGUIWindowGames() override = default;
+
+ // implementation of CGUIControl via CGUIMediaWindow
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // implementation of CGUIMediaWindow
+ void SetupShares() override;
+ bool OnClick(int iItem, const std::string& player = "") override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnAddMediaSource() override;
+ bool GetDirectory(const std::string& strDirectory, CFileItemList& items) override;
+ std::string GetStartFolder(const std::string& dir) override;
+
+ bool OnClickMsg(int controlId, int actionId);
+ void OnItemInfo(int itemNumber);
+ bool PlayGame(const CFileItem& item);
+
+ CGUIDialogProgress* m_dlgProgress = nullptr;
+};
+} // namespace GAME
+} // namespace KODI