summaryrefslogtreecommitdiffstats
path: root/xbmc/games/addons/GameClient.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/games/addons/GameClient.cpp')
-rw-r--r--xbmc/games/addons/GameClient.cpp730
1 files changed, 730 insertions, 0 deletions
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);
+}