diff options
Diffstat (limited to 'xbmc/games/addons/GameClient.cpp')
-rw-r--r-- | xbmc/games/addons/GameClient.cpp | 730 |
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); +} |