diff options
Diffstat (limited to 'xbmc/games/addons')
44 files changed, 5388 insertions, 0 deletions
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 |