diff options
Diffstat (limited to '')
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(), + [¤tPeripheral](const PERIPHERALS::PeripheralPtr& joystick) { + return joystick->Location() == currentPeripheral; + }); + } + + if (itJoystick == availableJoysticks.end()) + { + // Get the next most recently active joystick that doesn't have a current port + itJoystick = std::find_if( + availableJoysticks.begin(), availableJoysticks.end(), + [¤tPeripherals, &gameClientjoysticks](const PERIPHERALS::PeripheralPtr& joystick) { + const PeripheralLocation& joystickLocation = joystick->Location(); + + // If joystick doesn't have a current port, use it + auto itPeripheral = currentPeripherals.find(joystickLocation); + if (itPeripheral == currentPeripherals.end()) + return true; + + // Get the address of the last port this joystick was connected to + const PortAddress& portAddress = itPeripheral->second; + + // If port is disconnected, use this joystick + if (gameClientjoysticks.find(portAddress) == gameClientjoysticks.end()) + return true; + + return false; + }); + } + + // If found, assign the port and remove from the lists + if (itJoystick != availableJoysticks.end()) + { + // Dereference iterator + PERIPHERALS::PeripheralPtr peripheralJoystick = *itJoystick; + + // Map joystick + MapJoystick(std::move(peripheralJoystick), gameClientJoystick, result); + + // Remove from availableJoysticks list + availableJoysticks.erase(itJoystick); + } + else + { + // No joystick found, clear the port + gameClientJoystick->ClearSource(); + } + } + + return result; +} + +void CGameAgentManager::MapJoystick(PERIPHERALS::PeripheralPtr peripheralJoystick, + std::shared_ptr<CGameClientJoystick> gameClientJoystick, + PortMap& result) +{ + // Upcast peripheral joystick to input provider + JOYSTICK::IInputProvider* inputProvider = peripheralJoystick.get(); + + // Update game joystick's source peripheral + gameClientJoystick->SetSource(std::move(peripheralJoystick)); + + // Map input provider to input handler + result[inputProvider] = std::move(gameClientJoystick); +} + +void CGameAgentManager::LogPeripheralMap( + const PeripheralMap& peripheralMap, + const std::set<PERIPHERALS::PeripheralPtr>& disconnectedPeripherals) +{ + CLog::Log(LOGDEBUG, "===== Peripheral Map ====="); + + unsigned int line = 0; + + if (!peripheralMap.empty()) + { + for (const auto& [controllerAddress, peripheral] : peripheralMap) + { + if (line != 0) + CLog::Log(LOGDEBUG, ""); + CLog::Log(LOGDEBUG, "{}:", controllerAddress); + CLog::Log(LOGDEBUG, " {} [{}]", peripheral->Location(), peripheral->DeviceName()); + + ++line; + } + } + + if (!disconnectedPeripherals.empty()) + { + if (line != 0) + CLog::Log(LOGDEBUG, ""); + CLog::Log(LOGDEBUG, "Disconnected:"); + + // Sort by peripheral location + std::map<std::string, std::string> disconnectedPeripheralMap; + for (const auto& peripheral : disconnectedPeripherals) + disconnectedPeripheralMap[peripheral->Location()] = peripheral->DeviceName(); + + // Log location and device name for disconnected peripherals + for (const auto& [location, deviceName] : disconnectedPeripheralMap) + CLog::Log(LOGDEBUG, " {} [{}]", location, deviceName); + } + + CLog::Log(LOGDEBUG, "=========================="); +} 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 |