summaryrefslogtreecommitdiffstats
path: root/xbmc/games/addons
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
commitc04dcc2e7d834218ef2d4194331e383402495ae1 (patch)
tree7333e38d10d75386e60f336b80c2443c1166031d /xbmc/games/addons
parentInitial commit. (diff)
downloadkodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz
kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/games/addons')
-rw-r--r--xbmc/games/addons/CMakeLists.txt14
-rw-r--r--xbmc/games/addons/GameClient.cpp730
-rw-r--r--xbmc/games/addons/GameClient.h264
-rw-r--r--xbmc/games/addons/GameClientCallbacks.h38
-rw-r--r--xbmc/games/addons/GameClientInGameSaves.cpp164
-rw-r--r--xbmc/games/addons/GameClientInGameSaves.h66
-rw-r--r--xbmc/games/addons/GameClientProperties.cpp292
-rw-r--r--xbmc/games/addons/GameClientProperties.h93
-rw-r--r--xbmc/games/addons/GameClientSubsystem.cpp70
-rw-r--r--xbmc/games/addons/GameClientSubsystem.h81
-rw-r--r--xbmc/games/addons/GameClientTranslator.cpp256
-rw-r--r--xbmc/games/addons/GameClientTranslator.h115
-rw-r--r--xbmc/games/addons/cheevos/CMakeLists.txt5
-rw-r--r--xbmc/games/addons/cheevos/GameClientCheevos.cpp191
-rw-r--r--xbmc/games/addons/cheevos/GameClientCheevos.h57
-rw-r--r--xbmc/games/addons/input/CMakeLists.txt23
-rw-r--r--xbmc/games/addons/input/GameClientController.cpp138
-rw-r--r--xbmc/games/addons/input/GameClientController.h71
-rw-r--r--xbmc/games/addons/input/GameClientDevice.cpp73
-rw-r--r--xbmc/games/addons/input/GameClientDevice.h77
-rw-r--r--xbmc/games/addons/input/GameClientHardware.cpp25
-rw-r--r--xbmc/games/addons/input/GameClientHardware.h43
-rw-r--r--xbmc/games/addons/input/GameClientInput.cpp697
-rw-r--r--xbmc/games/addons/input/GameClientInput.h160
-rw-r--r--xbmc/games/addons/input/GameClientJoystick.cpp205
-rw-r--r--xbmc/games/addons/input/GameClientJoystick.h101
-rw-r--r--xbmc/games/addons/input/GameClientKeyboard.cpp101
-rw-r--r--xbmc/games/addons/input/GameClientKeyboard.h76
-rw-r--r--xbmc/games/addons/input/GameClientMouse.cpp106
-rw-r--r--xbmc/games/addons/input/GameClientMouse.h74
-rw-r--r--xbmc/games/addons/input/GameClientPort.cpp72
-rw-r--r--xbmc/games/addons/input/GameClientPort.h103
-rw-r--r--xbmc/games/addons/input/GameClientTopology.cpp110
-rw-r--r--xbmc/games/addons/input/GameClientTopology.h50
-rw-r--r--xbmc/games/addons/streams/CMakeLists.txt14
-rw-r--r--xbmc/games/addons/streams/GameClientStreamAudio.cpp105
-rw-r--r--xbmc/games/addons/streams/GameClientStreamAudio.h52
-rw-r--r--xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp40
-rw-r--r--xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h29
-rw-r--r--xbmc/games/addons/streams/GameClientStreamVideo.cpp108
-rw-r--r--xbmc/games/addons/streams/GameClientStreamVideo.h49
-rw-r--r--xbmc/games/addons/streams/GameClientStreams.cpp118
-rw-r--r--xbmc/games/addons/streams/GameClientStreams.h55
-rw-r--r--xbmc/games/addons/streams/IGameClientStream.h77
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