Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[games] Implement in-game saves #11380

Merged
merged 2 commits into from Feb 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions addons/kodi.game/addon.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon id="kodi.game" version="1.0.28" provider-name="Team-Kodi">
<backwards-compatibility abi="1.0.28"/>
<addon id="kodi.game" version="1.0.29" provider-name="Team-Kodi">
<backwards-compatibility abi="1.0.29"/>
<requires>
<import addon="xbmc.core" version="0.1.0"/>
</requires>
Expand Down
Expand Up @@ -233,7 +233,7 @@ GAME_ERROR CheatReset(void);
*
* \return the error, or GAME_ERROR_NO_ERROR if data was set to a valid buffer
*/
GAME_ERROR GetMemory(GAME_MEMORY type, const uint8_t** data, size_t* size);
GAME_ERROR GetMemory(GAME_MEMORY type, uint8_t** data, size_t* size);

/*!
* \brief Set a cheat code
Expand Down
6 changes: 3 additions & 3 deletions xbmc/addons/kodi-addon-dev-kit/include/kodi/kodi_game_types.h
Expand Up @@ -21,10 +21,10 @@
#define KODI_GAME_TYPES_H_

/* current game API version */
#define GAME_API_VERSION "1.0.28"
#define GAME_API_VERSION "1.0.29"

/* min. game API version */
#define GAME_MIN_API_VERSION "1.0.28"
#define GAME_MIN_API_VERSION "1.0.29"

#include <stddef.h>
#include <stdint.h>
Expand Down Expand Up @@ -476,7 +476,7 @@ typedef struct KodiToAddonFuncTable_Game
GAME_ERROR (__cdecl* Serialize)(uint8_t*, size_t);
GAME_ERROR (__cdecl* Deserialize)(const uint8_t*, size_t);
GAME_ERROR (__cdecl* CheatReset)(void);
GAME_ERROR (__cdecl* GetMemory)(GAME_MEMORY, const uint8_t**, size_t*);
GAME_ERROR (__cdecl* GetMemory)(GAME_MEMORY, uint8_t**, size_t*);
GAME_ERROR (__cdecl* SetCheat)(unsigned int, bool, const char*);
} KodiToAddonFuncTable_Game;

Expand Down
2 changes: 2 additions & 0 deletions xbmc/games/addons/CMakeLists.txt
@@ -1,4 +1,5 @@
set(SOURCES GameClient.cpp
GameClientInGameSaves.cpp
GameClientInput.cpp
GameClientKeyboard.cpp
GameClientMouse.cpp
Expand All @@ -8,6 +9,7 @@ set(SOURCES GameClient.cpp

set(HEADERS GameClient.h
GameClientCallbacks.h
GameClientInGameSaves.h
GameClientInput.h
GameClientKeyboard.h
GameClientMouse.h
Expand Down
7 changes: 7 additions & 0 deletions xbmc/games/addons/GameClient.cpp
Expand Up @@ -20,6 +20,7 @@

#include "GameClient.h"
#include "GameClientCallbacks.h"
#include "GameClientInGameSaves.h"
#include "GameClientInput.h"
#include "GameClientKeyboard.h"
#include "GameClientMouse.h"
Expand Down Expand Up @@ -276,6 +277,9 @@ bool CGameClient::OpenFile(const CFileItem& file, IGameAudioCallback* audio, IGa
if (!InitializeGameplay(file.GetPath(), audio, video))
return false;

m_inGameSaves.reset(new CGameClientInGameSaves(this, &m_struct));

This comment was marked as spam.

This comment was marked as spam.

m_inGameSaves->Load();

return true;
}

Expand Down Expand Up @@ -486,6 +490,9 @@ void CGameClient::CloseFile()

if (m_bIsPlaying)
{
m_inGameSaves->Save();
m_inGameSaves.reset();

try { LogError(m_struct.UnloadGame(), "UnloadGame()"); }
catch (...) { LogException("UnloadGame()"); }
}
Expand Down
4 changes: 4 additions & 0 deletions xbmc/games/addons/GameClient.h
Expand Up @@ -39,6 +39,7 @@ class CFileItem;
namespace GAME
{

class CGameClientInGameSaves;
class CGameClientInput;
class CGameClientKeyboard;
class CGameClientMouse;
Expand Down Expand Up @@ -161,6 +162,9 @@ class CGameClient : public ADDON::CAddonDll
std::unique_ptr<IGameClientPlayback> m_playback; // Interface to control playback
GAME_REGION m_region; // Region of the loaded game

// In-game saves
std::unique_ptr<CGameClientInGameSaves> m_inGameSaves;

This comment was marked as spam.

This comment was marked as spam.


// Input
std::map<int, std::unique_ptr<CGameClientInput>> m_ports;
std::unique_ptr<CGameClientKeyboard> m_keyboard;
Expand Down
162 changes: 162 additions & 0 deletions xbmc/games/addons/GameClientInGameSaves.cpp
@@ -0,0 +1,162 @@
/*
* Copyright (C) 2016-2017 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 "GameClientInGameSaves.h"

#include "GameClient.h"
#include "GameClientTranslator.h"
#include "filesystem/File.h"
#include "filesystem/Directory.h"
#include "profiles/ProfilesManager.h"
#include "utils/URIUtils.h"
#include "utils/log.h"

#include <assert.h>

using namespace GAME;

#define INGAME_SAVES_DIRECTORY "InGameSaves"
#define INGAME_SAVES_EXTENSION_SAVE_RAM ".sav"
#define INGAME_SAVES_EXTENSION_RTC ".rtc"

This comment was marked as spam.


CGameClientInGameSaves::CGameClientInGameSaves(CGameClient* addon, const KodiToAddonFuncTable_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)
{
std::string path = URIUtils::AddFileToFolder(CProfilesManager::GetInstance().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->GetMemory(memoryType, &gameMemory, &size);
}
catch (...)
{
CLog::Log(LOGERROR, "GAME: %s: Exception caught in GetMemory()", m_gameClient->ID().c_str());
}

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 (%s) loaded from %s", CGameClientTranslator::ToString(memoryType), path.c_str());
}
else
{
CLog::Log(LOGERROR, "GAME: Failed to read in-game saves (%s): %ld/%ld bytes read", CGameClientTranslator::ToString(memoryType), read, size);
}
}
else
{
CLog::Log(LOGERROR, "GAME: Unable to open in-game saves (%s) from file %s", CGameClientTranslator::ToString(memoryType), path.c_str());
}
}
else
{
CLog::Log(LOGDEBUG, "GAME: No in-game saves (%s) to load", CGameClientTranslator::ToString(memoryType));
}
}

void CGameClientInGameSaves::Save(GAME_MEMORY memoryType)
{
uint8_t *gameMemory = nullptr;
size_t size = 0;

try
{
m_dllStruct->GetMemory(memoryType, &gameMemory, &size);
}
catch (...)
{
CLog::Log(LOGERROR, "GAME: %s: Exception caught in GetMemory()", m_gameClient->ID().c_str());
}

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 (%s) written to %s", CGameClientTranslator::ToString(memoryType), path.c_str());
}
else
{
CLog::Log(LOGERROR, "GAME: Failed to write in-game saves (%s): %ld/%ld bytes written", CGameClientTranslator::ToString(memoryType), written, size);
}
}
else
{
CLog::Log(LOGERROR, "GAME: Unable to open in-game saves (%s) from file %s", CGameClientTranslator::ToString(memoryType), path.c_str());
}
}
else
{
CLog::Log(LOGDEBUG, "GAME: No in-game saves (%s) to save", CGameClientTranslator::ToString(memoryType));
}
}
73 changes: 73 additions & 0 deletions xbmc/games/addons/GameClientInGameSaves.h
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2016-2017 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-addon-dev-kit/include/kodi/kodi_game_types.h"

#include <string>

struct GameClient;

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 KodiToAddonFuncTable_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 KodiToAddonFuncTable_Game* const m_dllStruct;
};
}
19 changes: 19 additions & 0 deletions xbmc/games/addons/GameClientTranslator.cpp
Expand Up @@ -39,6 +39,25 @@ const char* CGameClientTranslator::ToString(GAME_ERROR error)
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";
}

AVPixelFormat CGameClientTranslator::TranslatePixelFormat(GAME_PIXEL_FORMAT format)
{
switch (format)
Expand Down
7 changes: 7 additions & 0 deletions xbmc/games/addons/GameClientTranslator.h
Expand Up @@ -46,6 +46,13 @@ namespace GAME
*/
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 pixel format (Game API to FFMPEG).
* \param format The pixel format to translate.
Expand Down