Browse files

[savestates] Added game state persistence built on a new save game da…

…tabase. RetroPlayer can automatically load previous game state on file open, and 9 slots are available for hotkey-based save state storage.
  • Loading branch information...
1 parent 203cfa3 commit 3b014277c8428a587319a072956866769af257ed @garbear committed Mar 20, 2013
View
1 Makefile.in
@@ -50,6 +50,7 @@ DIRECTORY_ARCHIVES=$(DVDPLAYER_ARCHIVES) \
xbmc/filesystem/filesystem.a \
xbmc/games/games.a \
xbmc/games/libretro/libretro.a \
+ xbmc/games/savegames/savegames.a \
xbmc/games/tags/gameinfotags.a \
xbmc/games/windows/gamewindows.a \
xbmc/guilib/guilib.a \
View
24 language/English/strings.po
@@ -5978,7 +5978,29 @@ msgctxt "#15022"
msgid "Maximum rewind time"
msgstr ""
-#empty strings from id 15023 to 15051
+#empty string with id 15023
+
+#: xbmc/settings/GUISettings.cpp
+msgctxt "#15024"
+msgid "Automatically load previous game state"
+msgstr ""
+
+#: xbmc/settings/GUISettings.cpp
+msgctxt "#15025"
+msgid "Save game state every 30 seconds"
+msgstr ""
+
+#: xbmc/games/savegames/Savegame.cpp
+msgctxt "#15026"
+msgid "Autosave on %s"
+msgstr ""
+
+#: xbmc/games/savegames/Savegame.cpp
+msgctxt "#15027"
+msgid "Slot %d"
+msgstr ""
+
+#empty strings from id 15028 to 15051
msgctxt "#15052"
msgid "Password"
View
4 project/VS2010Express/XBMC.vcxproj
@@ -600,6 +600,8 @@
<ClCompile Include="..\..\xbmc\games\GameFileLoader.cpp" />
<ClCompile Include="..\..\xbmc\games\GameManager.cpp" />
<ClCompile Include="..\..\xbmc\games\libretro\LibretroEnvironment.cpp" />
+ <ClCompile Include="..\..\xbmc\games\savegames\SavestateDatabase.cpp" />
+ <ClCompile Include="..\..\xbmc\games\savegames\Savestate.cpp" />
<ClCompile Include="..\..\xbmc\games\SerialState.cpp" />
<ClCompile Include="..\..\xbmc\games\windows\GUIViewStateWindowGames.cpp" />
<ClCompile Include="..\..\xbmc\games\windows\GUIWindowGames.cpp" />
@@ -1079,6 +1081,8 @@
<ClInclude Include="..\..\xbmc\games\GameManager.h" />
<ClInclude Include="..\..\xbmc\games\libretro\libretro.h" />
<ClInclude Include="..\..\xbmc\games\libretro\LibretroEnvironment.h" />
+ <ClInclude Include="..\..\xbmc\games\savegames\SavestateDatabase.h" />
+ <ClInclude Include="..\..\xbmc\games\savegames\Savestate.h" />
<ClInclude Include="..\..\xbmc\games\SerialState.h" />
<ClInclude Include="..\..\xbmc\games\windows\GUIViewStateWindowGames.h" />
<ClInclude Include="..\..\xbmc\games\windows\GUIWindowGames.h" />
View
15 project/VS2010Express/XBMC.vcxproj.filters
@@ -304,6 +304,9 @@
<Filter Include="dbwrappers\test">
<UniqueIdentifier>{43dbf5f1-ffdc-4fd5-8f6c-5f38f89b192f}</UniqueIdentifier>
</Filter>
+ <Filter Include="games\savegames">
+ <UniqueIdentifier>{42e2939e-467c-4c7f-bb3e-b4860c19e9e0}</UniqueIdentifier>
+ </Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\xbmc\win32\pch.cpp">
@@ -3063,6 +3066,12 @@
<ClCompile Include="..\..\xbmc\dbwrappers\test\TestDynamicDatabase.cpp">
<Filter>dbwrappers\test</Filter>
</ClCompile>
+ <ClCompile Include="..\..\xbmc\games\savegames\SavestateDatabase.cpp">
+ <Filter>games\savegames</Filter>
+ </ClCompile>
+ <ClCompile Include="..\..\xbmc\games\savegames\Savestate.cpp">
+ <Filter>games\savegames</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\xbmc\win32\pch.h">
@@ -5972,6 +5981,12 @@
<ClInclude Include="..\..\xbmc\utils\IDeserializable.h">
<Filter>utils</Filter>
</ClInclude>
+ <ClInclude Include="..\..\xbmc\games\savegames\SavestateDatabase.h">
+ <Filter>games\savegames</Filter>
+ </ClInclude>
+ <ClInclude Include="..\..\xbmc\games\savegames\Savestate.h">
+ <Filter>games\savegames</Filter>
+ </ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\..\xbmc\win32\XBMC_PC.rc">
View
19 system/keymaps/keyboard.xml
@@ -262,6 +262,25 @@
<d>JoypadY</d>
<a>JoypadL</a>
<s>JoypadR</s>
+ <one>Load1</one>
+ <two>Load2</two>
+ <three>Load3</three>
+ <four>Load4</four>
+ <five>Load5</five>
+ <six>Load6</six>
+ <seven>Load7</seven>
+ <eight>Load8</eight>
+ <nine>Load9</nine>
+ <!-- US keyboard. May need changes for other locales -->
+ <exclaim>Save1</exclaim>
+ <at>Save2</at>
+ <hash>Save3</hash>
+ <dollar>Save4</dollar>
+ <percent>Save5</percent>
+ <caret>Save6</caret>
+ <ampersand>Save7</ampersand>
+ <asterisk>Save8</asterisk>
+ <leftbracket>Save9</leftbracket>
</keyboard>
</FullscreenGame>
<VideoTimeSeek>
View
7 xbmc/Application.cpp
@@ -2890,6 +2890,13 @@ bool CApplication::OnAction(const CAction &action)
}
}
+ if (IsPlayingGame() && ((ACTION_SAVE <= action.GetID() && action.GetID() <= ACTION_SAVE9) ||
+ (ACTION_LOAD <= action.GetID() && action.GetID() <= ACTION_LOAD9)))
+ {
+ m_pPlayer->OnAction(action.GetID());
+ return true;
+ }
+
if (g_peripherals.OnAction(action))
return true;
View
2 xbmc/DatabaseManager.cpp
@@ -27,6 +27,7 @@
#include "video/VideoDatabase.h"
#include "pvr/PVRDatabase.h"
#include "epg/EpgDatabase.h"
+#include "games/savegames/SavestateDatabase.h"
#include "settings/AdvancedSettings.h"
using namespace std;
@@ -63,6 +64,7 @@ void CDatabaseManager::Initialize(bool addonsOnly)
{ CVideoDatabase db; UpdateDatabase(db, &g_advancedSettings.m_databaseVideo); }
{ CPVRDatabase db; UpdateDatabase(db, &g_advancedSettings.m_databaseTV); }
{ CEpgDatabase db; UpdateDatabase(db, &g_advancedSettings.m_databaseEpg); }
+ { CSavestateDatabase db; UpdateDatabase(db, &g_advancedSettings.m_databaseSavestates); }
CLog::Log(LOGDEBUG, "%s, updating databases... DONE", __FUNCTION__);
}
View
4 xbmc/FileItem.cpp
@@ -534,6 +534,7 @@ const CFileItem& CFileItem::operator=(const CFileItem& item)
m_lStartOffset = item.m_lStartOffset;
m_lStartPartNumber = item.m_lStartPartNumber;
m_lEndOffset = item.m_lEndOffset;
+ m_startSaveState = item.m_startSaveState;
m_strDVDLabel = item.m_strDVDLabel;
m_strTitle = item.m_strTitle;
m_iprogramCount = item.m_iprogramCount;
@@ -571,6 +572,7 @@ void CFileItem::Reset()
m_lStartOffset = 0;
m_lStartPartNumber = 1;
m_lEndOffset = 0;
+ m_startSaveState.Empty();
m_iprogramCount = 0;
m_idepth = 1;
m_iLockMode = LOCK_MODE_EVERYONE;
@@ -621,6 +623,7 @@ void CFileItem::Archive(CArchive& ar)
ar << m_lStartOffset;
ar << m_lStartPartNumber;
ar << m_lEndOffset;
+ ar << m_startSaveState;
ar << m_iLockMode;
ar << m_strLockCode;
ar << m_iBadPwdCount;
@@ -675,6 +678,7 @@ void CFileItem::Archive(CArchive& ar)
ar >> m_lStartOffset;
ar >> m_lStartPartNumber;
ar >> m_lEndOffset;
+ ar >> m_startSaveState;
int temp;
ar >> temp;
m_iLockMode = (LockType)temp;
View
1 xbmc/FileItem.h
@@ -398,6 +398,7 @@ class CFileItem :
int m_lStartOffset;
int m_lStartPartNumber;
int m_lEndOffset;
+ CStdString m_startSaveState;
LockType m_iLockMode;
CStdString m_strLockCode;
int m_iHasLock; // 0 - no lock 1 - lock, but unlocked 2 - locked
View
6 xbmc/GUIInfoManager.cpp
@@ -4346,6 +4346,12 @@ CStdString CGUIInfoManager::GetItemLabel(const CFileItem *item, int info, CStdSt
if (item->GetMusicInfoTag()->GetDuration() > 0)
duration = StringUtils::SecondsToTimeString(item->GetMusicInfoTag()->GetDuration());
}
+ else if (item->HasProperty("duration"))
+ {
+ // The "duration" property is set for savestate file items, as they
+ // have no dedicated info tag.
+ duration = StringUtils::SecondsToTimeString((long)item->GetProperty("duration").asInteger());
+ }
return duration;
}
case LISTITEM_PLOT:
View
55 xbmc/cores/RetroPlayer/RetroPlayer.cpp
@@ -33,6 +33,7 @@
#include "guilib/GUIWindowManager.h"
#include "guilib/Key.h"
#include "settings/GUISettings.h"
+#include "threads/SystemClock.h" // Should auto-save tracking be in GameClient.cpp?
#include "URL.h"
#include "utils/log.h"
#include "utils/StringUtils.h"
@@ -72,7 +73,17 @@ bool CRetroPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options
m_bStop = false;
if (IsRunning())
+ {
+ // If the same file was provided, load the appropriate save state
+ if (m_gameClient && file.GetPath().Equals(m_file.GetPath()))
+ {
+ if (!file.m_startSaveState.empty())
+ return m_gameClient->Load(file.m_startSaveState);
+ else
+ return m_gameClient->AutoLoad();
+ }
CloseFile();
+ }
// Get game info tag (from a mutable file item, if necessary)
const GAME_INFO::CGameInfoTag *tag = file.GetGameInfoTag();
@@ -203,6 +214,7 @@ bool CRetroPlayer::InstallGameClient(CFileItem file, GameClientPtr &result) cons
}
}
}
+
file.ClearProperty("gameclient"); // don't want this to interfere later on
}
@@ -401,6 +413,38 @@ bool CRetroPlayer::OnAction(const CAction &action)
if (!IsPlaying())
return false;
+ switch (action.GetID())
+ {
+ case ACTION_SAVE:
+ return m_gameClient->AutoSave();
+ case ACTION_SAVE1:
+ case ACTION_SAVE2:
+ case ACTION_SAVE3:
+ case ACTION_SAVE4:
+ case ACTION_SAVE5:
+ case ACTION_SAVE6:
+ case ACTION_SAVE7:
+ case ACTION_SAVE8:
+ case ACTION_SAVE9:
+ return m_gameClient->Save(action.GetID() - ACTION_SAVE1 + 1);
+ case ACTION_LOAD:
+ if (m_playSpeed <= 0)
+ ToFFRW(1);
+ return m_gameClient->AutoLoad();
+ case ACTION_LOAD1:
+ case ACTION_LOAD2:
+ case ACTION_LOAD3:
+ case ACTION_LOAD4:
+ case ACTION_LOAD5:
+ case ACTION_LOAD6:
+ case ACTION_LOAD7:
+ case ACTION_LOAD8:
+ case ACTION_LOAD9:
+ if (m_playSpeed <= 0)
+ ToFFRW(1);
+ return m_gameClient->Load(action.GetID() - ACTION_LOAD1 + 1);
+ }
+
return false;
}
@@ -441,6 +485,8 @@ void CRetroPlayer::Process()
m_video.GoForth(framerate, m_PlayerOptions.fullscreen);
+ unsigned int saveTimer = XbmcThreads::SystemClockMillis();
+
const double frametime = 1000 * 1000 / framerate; // microseconds
double nextpts = CDVDClock::GetAbsoluteClock() + frametime;
CLog::Log(LOGDEBUG, "RetroPlayer: Beginning loop de loop");
@@ -469,6 +515,13 @@ void CRetroPlayer::Process()
// If the game client uses single frame audio, render those now
m_audio.Flush();
+ if (g_guiSettings.GetBool("games.autosave") &&
+ XbmcThreads::SystemClockMillis() - saveTimer > 30000) // every 30 seconds
+ {
+ m_gameClient->AutoSave();
+ saveTimer = XbmcThreads::SystemClockMillis();
+ }
+
// Slow down (increase nextpts) if we're playing catchup after stalling
if (nextpts < CDVDClock::GetAbsoluteClock())
nextpts = CDVDClock::GetAbsoluteClock();
@@ -485,6 +538,8 @@ void CRetroPlayer::Process()
}
m_bStop = true;
+
+ // Save the game before the video cuts out
m_gameClient->CloseFile();
m_video.StopThread(true);
View
3 xbmc/cores/RetroPlayer/RetroPlayer.h
@@ -113,6 +113,9 @@ class CRetroPlayer : public IPlayer, public CThread
virtual int64_t GetTime();
virtual int64_t GetTotalTime();
+ bool Save(unsigned int slot) { return m_gameClient && m_gameClient->Save(slot); }
+ bool Save(const CStdString &label) { return m_gameClient && m_gameClient->Save(label); }
+ bool Load(const CStdString &saveStatePath) { return m_gameClient && m_gameClient->Load(saveStatePath); }
protected:
virtual void Process();
View
2 xbmc/filesystem/SpecialProtocol.cpp
@@ -134,6 +134,8 @@ CStdString CSpecialProtocol::TranslatePath(const CURL &url)
URIUtils::AddFileToFolder(g_settings.GetDatabaseFolder(), FileName, translatedPath);
else if (RootDir.Equals("thumbnails"))
URIUtils::AddFileToFolder(g_settings.GetThumbnailsFolder(), FileName, translatedPath);
+ else if (RootDir.Equals("savegames"))
+ URIUtils::AddFileToFolder(g_settings.GetSavegamesFolder(), FileName, translatedPath);
else if (RootDir.Equals("recordings") || RootDir.Equals("cdrips"))
URIUtils::AddFileToFolder(g_guiSettings.GetString("audiocds.recordingpath", false), FileName, translatedPath);
else if (RootDir.Equals("screenshots"))
View
235 xbmc/games/GameClient.cpp
@@ -22,7 +22,7 @@
#include "GameClient.h"
#include "addons/AddonManager.h"
-#include "filesystem/Directory.h"
+#include "games/savegames/SavestateDatabase.h"
#include "libretro/LibretroEnvironment.h"
#include "settings/GUISettings.h"
#include "threads/SingleLock.h"
@@ -311,28 +311,52 @@ bool CGameClient::OpenFile(const CFileItem& file, const DataReceiver &callbacks)
m_frameRate = fps;
m_sampleRate = sampleRate;
+ m_rewindSupported = g_guiSettings.GetBool("games.enablerewind");
- // Check if save states are supported, so rewind can be used.
+ // Check if save states are supported, so savestates and rewind can be used.
size_t state_size = m_dll.retro_serialize_size();
- m_rewindSupported = state_size && g_guiSettings.GetBool("games.enablerewind");
- if (m_rewindSupported)
+ bool initSuccess = InitSaveState(info.data, info.size);
+ if (state_size)
{
- m_serialState.Init(state_size, (size_t)(g_guiSettings.GetInt("games.rewindtime") * m_frameRate));
- if (!m_dll.retro_serialize(m_serialState.GetState(), m_serialState.GetFrameSize()))
+ if (g_guiSettings.GetBool("games.savestates"))
{
- m_rewindSupported = false;
- CLog::Log(LOGINFO, "GameClient: Unable to serialize state, proceeding without rewind");
+ if (!initSuccess)
+ CLog::Log(LOGERROR, "GameClient: Couldn't open database, continuing without save support");
+ else
+ {
+ // Load savestate if possible
+ bool loadSuccess = false;
+ if (!file.m_startSaveState.empty())
+ loadSuccess = Load(file.m_startSaveState);
+ if (!loadSuccess)
+ loadSuccess = AutoLoad();
+ else
+ loadSuccess = AutoLoad();
+ if (!loadSuccess && m_rewindSupported)
+ {
+ CLog::Log(LOGDEBUG, "GameClient: Failed to load last savestate, forcing rewind to off");
+ m_rewindSupported = false;
+ }
+ }
}
- else
+ // Set up rewind functionality
+ if (m_rewindSupported)
{
- CLog::Log(LOGINFO, "GameClient: Rewind is enabled");
+ m_serialState.Init(state_size, (size_t)(g_guiSettings.GetInt("games.rewindtime") * m_frameRate));
- // Load save and auto state
+ if (m_dll.retro_serialize(m_serialState.GetState(), m_serialState.GetFrameSize()))
+ CLog::Log(LOGDEBUG, "GameClient: Rewind is enabled");
+ else
+ {
+ m_rewindSupported = false;
+ m_serialState.Reset();
+ CLog::Log(LOGDEBUG, "GameClient: Unable to serialize state, proceeding without rewind");
+ }
}
}
else
{
- CLog::Log(LOGINFO, "GameClient: Rewind support is not enabled");
+ CLog::Log(LOGINFO, "GameClient: Game serialization not supported");
}
// Query the game region
@@ -370,9 +394,13 @@ void CGameClient::CloseFile()
if (m_dll.IsLoaded() && m_bIsPlaying)
{
+ if (g_guiSettings.GetBool("games.savestates"))
+ AutoSave();
m_dll.retro_unload_game();
m_bIsPlaying = false;
}
+ m_gamePath.clear();
+ m_saveState.Reset();
CLibretroEnvironment::ResetCallbacks();
}
@@ -405,6 +433,9 @@ void CGameClient::RunFrame()
m_dll.retro_run();
+ m_saveState.SetPlaytimeFrames(m_saveState.GetPlaytimeFrames() + 1);
+ m_saveState.SetPlaytimeWallClock(m_saveState.GetPlaytimeWallClock() + 1.0 / m_frameRate);
+
// Append a new state delta to the rewind buffer
if (m_rewindSupported)
{
@@ -419,6 +450,169 @@ void CGameClient::RunFrame()
}
}
+bool CGameClient::InitSaveState(const void *gameBuffer /* = NULL */, size_t length /* = 0 */)
+{
+ // Reset the database ID. This makes sure adding a new record doesn't erase an old one
+ m_saveState.SetDatabaseId(-1);
+ m_saveState.SetGameClient(ID());
+ m_saveState.SetGamePath(m_gamePath);
+
+ if (m_saveState.GetGameCRC().empty())
+ {
+ // Check the database for game CRC first
+ CSavestateDatabase db;
+ CStdString strCrc;
+ if (db.Open() && db.GetCRC(m_gamePath, strCrc))
+ {
+ m_saveState.SetGameCRC(strCrc);
+ }
+ else
+ {
+ // Path not in database, calculate the game CRC now
+ CLog::Log(LOGDEBUG, "GameClient: %s not in database, trying CRC", URIUtils::GetFileName(m_gamePath).c_str());
+ if (gameBuffer)
+ m_saveState.SetGameCRCFromFile(reinterpret_cast<const char*>(gameBuffer), length);
+ else
+ m_saveState.SetGameCRCFromFile(m_gamePath);
+ }
+ }
+
+ if (m_saveState.GetGameCRC().empty())
+ {
+ CLog::Log(LOGERROR, "GameClient: Failed to calculate CRC for %s", URIUtils::GetFileName(m_gamePath).c_str());
+ return false;
+ }
+ return true;
+}
+
+bool CGameClient::AutoLoad()
+{
+ if (!m_bIsPlaying)
+ return false; // libretro DLL would probably crash
+ CSingleLock lock(m_critSection);
+ CLog::Log(LOGINFO, "GameClient: Auto-loading last save state");
+ if (!InitSaveState())
+ return false;
+ m_saveState.SetSaveTypeAuto();
+ return Load();
+}
+
+bool CGameClient::Load(unsigned int slot)
+{
+ if (!m_bIsPlaying)
+ return false; // libretro DLL would probably crash
+ CSingleLock lock(m_critSection);
+ CLog::Log(LOGINFO, "GameClient: Loading save state from slot %u", slot);
+ if (!InitSaveState())
+ return false;
+ m_saveState.SetSaveTypeSlot(slot);
+ return Load();
+}
+
+bool CGameClient::Load(const CStdString &saveStatePath)
+{
+ if (!m_bIsPlaying)
+ return false; // libretro DLL would probably crash
+ CSingleLock lock(m_critSection);
+ CLog::Log(LOGINFO, "GameClient: Loading save state %s", saveStatePath.c_str());
+ m_saveState.SetPath(saveStatePath);
+ return Load();
+}
+
+bool CGameClient::Load()
+{
+ std::vector<uint8_t> data;
+ CSavestate savestate;
+ CSavestateDatabase db;
+ if (m_saveState.Read(data) && db.Open() && db.GetObjectByIndex("path", m_saveState.GetPath(), &savestate))
+ {
+ if (!m_dll.retro_unserialize(data.data(), data.size()))
+ {
+ CLog::Log(LOGERROR, "GameClient: Libretro core failed to de-serialize data!");
+ return false;
+ }
+ // Reset rewind buffer if rewinding is enabled
+ if (m_rewindSupported && m_serialState.IsInited())
+ {
+ m_serialState.Init(data.size(), (size_t)(g_guiSettings.GetInt("games.rewindtime") * m_frameRate));
+ memcpy(m_serialState.GetState(), data.data(), data.size());
+ }
+ m_saveState = savestate;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "GameClient: Failed to read save state or load from database");
+ }
+ // Return true even if Read() failed, because the next call the Save() will probably succeed
+ return true;
+}
+
+bool CGameClient::AutoSave()
+{
+ if (!m_bIsPlaying)
+ return false;
+ CSingleLock lock(m_critSection);
+ CLog::Log(LOGINFO, "GameClient: Auto-save");
+ if (!InitSaveState())
+ return false;
+ m_saveState.SetSaveTypeAuto();
+ return Save();
+}
+
+bool CGameClient::Save(unsigned int slot)
+{
+ if (!m_bIsPlaying)
+ return false;
+ CSingleLock lock(m_critSection);
+ CLog::Log(LOGINFO, "GameClient: Saving state to slot %u", slot);
+ if (!InitSaveState())
+ return false;
+
+ // Avoid duplicate labels. If saving to "Slot 2", and a manual save is
+ // labeled "Slot 2", delete the manual label.
+ m_saveState.SetSaveTypeSlot(slot); // Generate temporary slot label
+ m_saveState.SetSaveTypeLabel(m_saveState.GetLabel());
+ CSavestateDatabase db;
+ db.DeleteSaveState(m_saveState.GetPath(), false);
+
+ m_saveState.SetSaveTypeSlot(slot);
+ return Save();
+}
+
+bool CGameClient::Save(const CStdString &label)
+{
+ if (!m_bIsPlaying)
+ return false;
+ CSingleLock lock(m_critSection);
+ CLog::Log(LOGINFO, "GameClient: Saving state with label %s", label.c_str());
+ if (!InitSaveState())
+ return false;
+ m_saveState.SetSaveTypeLabel(label);
+ return Save();
+}
+
+bool CGameClient::Save()
+{
+ // Prefer serialized states to avoid any game client serialization procedures
+ if (m_rewindSupported && m_serialState.GetFrameSize())
+ {
+ m_savestateBuffer.resize(m_serialState.GetFrameSize());
+ memcpy(m_savestateBuffer.data(), m_serialState.GetState(), m_serialState.GetFrameSize());
+ }
+ else
+ {
+ size_t save_size = m_dll.retro_serialize_size();
+ if (!save_size)
+ return false;
+ m_savestateBuffer.resize(save_size);
+ if (!m_dll.retro_serialize(m_savestateBuffer.data(), m_savestateBuffer.size()))
+ return false;
+ }
+
+ CSavestateDatabase db;
+ return db.Save(m_saveState, m_savestateBuffer);
+}
+
unsigned int CGameClient::RewindFrames(unsigned int frames)
{
unsigned int rewound = 0;
@@ -427,8 +621,17 @@ unsigned int CGameClient::RewindFrames(unsigned int frames)
CSingleLock lock(m_critSection);
rewound = m_serialState.RewindFrames(frames);
- if (rewound)
- m_dll.retro_unserialize(m_serialState.GetState(), m_serialState.GetFrameSize());
+ if (rewound && m_dll.retro_unserialize(m_serialState.GetState(), m_serialState.GetFrameSize()))
+ {
+ // We calculate these separately because they can actually diverge, as
+ // the framerate is possibly variable and can depend on the chosen audio
+ // samplerate (I'm not sure how likely this is however)
+ uint64_t frames = m_saveState.GetPlaytimeFrames();
+ m_saveState.SetPlaytimeFrames(frames > rewound ? frames - rewound : 0);
+
+ double wallclock = m_saveState.GetPlaytimeWallClock();
+ m_saveState.SetPlaytimeWallClock(wallclock > rewound / m_frameRate ? wallclock - rewound / m_frameRate : 0.0);
+ }
}
return rewound;
}
@@ -441,6 +644,10 @@ void CGameClient::Reset()
// resets controllers to JOYPAD after a reset, so guard against this.
m_dll.retro_reset();
+ InitSaveState();
+ m_saveState.SetPlaytimeFrames(0);
+ m_saveState.SetPlaytimeWallClock(0.0);
+
if (m_rewindSupported)
{
m_serialState.Init(m_serialState.GetFrameSize(), m_serialState.GetMaxFrames());
View
61 xbmc/games/GameClient.h
@@ -25,6 +25,7 @@
#include "FileItem.h"
#include "GameClientDLL.h"
#include "GameFileLoader.h"
+#include "games/savegames/Savestate.h"
#include "games/tags/GameInfoTagLoader.h"
#include "SerialState.h"
#include "threads/CriticalSection.h"
@@ -156,6 +157,47 @@ namespace ADDON
void RunFrame();
/**
+ * Load the serialized state from the auto-save slot (filename looks like
+ * feba62c2.savestate). Returns true if the next call to Load() or Save()
+ * is expected to succeed (such as if the file can't be loaded because it
+ * doesn't exist, but Save() will create the file and Load() and Save()
+ * will work after that).
+ *
+ * Savestates are placed in special://savegames/gameclient.id/
+ */
+ bool AutoLoad();
+
+ /**
+ * Load the serialized state from the numbered slot (filename looks like
+ * feba62c2_1.savestate).
+ */
+ bool Load(unsigned int slot);
+
+ /**
+ * Load the serialized state from the specified path.
+ */
+ bool Load(const CStdString &saveStatePath);
+
+ /**
+ * Commit the current serialized state to the local drive (filename looks
+ * like feba62c2.savestate).
+ */
+ bool AutoSave();
+
+ /**
+ * Commit the current serialized state to the local drive (filename looks
+ * like feba62c2_1.savestate).
+ */
+ bool Save(unsigned int slot);
+
+ /**
+ * Commit the current serialized state to the local drive. The CRC of the
+ * label is concatenated to the CRC of the game file, and the resulting
+ * filename looks like feba62c2_bdcb488a.savestate
+ */
+ bool Save(const CStdString &label);
+
+ /**
* Rewind gameplay 'frames' frames.
* As there is a fixed size buffer backing
* save state deltas, it might not be possible to rewind as many
@@ -187,6 +229,21 @@ namespace ADDON
void Initialize();
/**
+ * Init the savestate file by setting the game path, game client and game
+ * CRC.
+ *
+ * gameBuffer and length are convenience variables to avoid hitting the
+ * disk for CRC calculation when the game file is already loaded in RAM.
+ */
+ bool InitSaveState(const void *gameBuffer = NULL, size_t length = 0);
+
+ // Internal load function.
+ bool Load();
+
+ // Internal save function.
+ bool Save();
+
+ /**
* Given the strategies above, order them in the way that respects
* g_guiSettings.GetBool("gamesdebug.prefervfs").
*/
@@ -218,6 +275,10 @@ namespace ADDON
CCriticalSection m_critSection;
bool m_rewindSupported;
CSerialState m_serialState;
+ CSavestate m_saveState;
+
+ // If rewinding is disabled, use a buffer to avoid re-allocation when saving games
+ std::vector<uint8_t> m_savestateBuffer;
/**
* This callback exists to give XBMC a chance to poll for input. XBMC already
View
34 xbmc/games/GameManager.cpp
@@ -26,7 +26,9 @@
#include "dialogs/GUIDialogYesNo.h"
#include "filesystem/Directory.h"
#include "GameFileLoader.h"
+#include "games/savegames/Savestate.h"
#include "guilib/GUIWindowManager.h"
+#include "settings/Settings.h"
#include "threads/SingleLock.h"
#include "URL.h"
#include "utils/log.h"
@@ -36,6 +38,7 @@
using namespace ADDON;
using namespace GAME_INFO;
+using namespace XFILE;
/* TEMPORARY */
@@ -121,6 +124,15 @@ void CGameManager::RegisterAddon(GameClientPtr clientAddon, bool launchQueued /*
m_queuedFile = CFileItem();
}
}
+
+ // Check to see if the savegame folder for this game client exists
+ CStdString savegameFolder;
+ URIUtils::AddFileToFolder(g_settings.GetSavegamesFolder(), clientAddon->ID(), savegameFolder);
+ if (!CDirectory::Exists(savegameFolder))
+ {
+ CLog::Log(LOGINFO, "Create new savegames folder: %s", savegameFolder.c_str());
+ CDirectory::Create(savegameFolder);
+ }
}
void CGameManager::UnregisterAddonByID(const CStdString &ID)
@@ -255,15 +267,35 @@ void CGameManager::GetGameClientIDs(const CFileItem& file, CStdStringArray &cand
CSingleLock lock(m_critSection);
CStdString gameclient = file.GetProperty("gameclient").asString();
+
+ // If a start save state was specified, validate the candidate against the
+ // save state's game client
+ if (!file.m_startSaveState.empty())
+ {
+ CSavestate savestate;
+ savestate.SetPath(file.m_startSaveState);
+ if (!savestate.GetGameClient().empty())
+ {
+ if (gameclient.empty())
+ gameclient = savestate.GetGameClient(); // Use new game client as filter below
+ else if (gameclient != savestate.GetGameClient())
+ return; // New game client doesn't match, no valid candidates
+ }
+ }
+
for (std::vector<GameClientConfig>::const_iterator it = m_gameClients.begin(); it != m_gameClients.end(); it++)
{
+ if (!gameclient.empty() && gameclient != it->id)
+ continue;
+
CLog::Log(LOGDEBUG, "GameManager: To open or not to open using %s, that is the question", it->id.c_str());
if (CGameFileLoader::CanOpen(file, *it, true))
{
CLog::Log(LOGDEBUG, "GameManager: Adding client %s as a candidate", it->id.c_str());
candidates.push_back(it->id);
}
- if (!gameclient.empty() && it->id == gameclient)
+
+ if (!gameclient.empty())
break; // If the game client isn't installed, it's not a valid candidate
}
}
View
7 xbmc/games/savegames/Makefile
@@ -0,0 +1,7 @@
+SRCS=Savestate.cpp \
+ SavestateDatabase.cpp
+
+LIB=savegames.a
+
+include ../../../Makefile.include
+-include $(patsubst %.cpp,%.P,$(patsubst %.c,%.P,$(SRCS)))
View
372 xbmc/games/savegames/Savestate.cpp
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2005-2013 Team XBMC
+ * http://www.xbmc.org
+ *
+ * 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 XBMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "Savestate.h"
+#include "Application.h"
+#include "cores/VideoRenderers/RenderManager.h"
+#include "cores/VideoRenderers/RenderCapture.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "pictures/Picture.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "utils/Crc32.h"
+#include "utils/log.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "XBDateTime.h"
+
+#define SAVESTATE_EXTENSION ".savestate"
+#define SAVESTATE_THUMB_WIDTH (g_advancedSettings.GetThumbSize())
+
+using namespace XFILE;
+
+void CSavestate::Reset()
+{
+ m_path.clear();
+ m_databaseId = -1;
+ m_bAutoSave = true;
+ m_slot = 0;
+ m_label.clear();
+ m_size = 0;
+ m_gamePath.clear();
+ m_gameClient.clear();
+ m_gameCRC.clear();
+ m_playtimeFrames = 0;
+ m_playtimeWallClock = 0.0;
+ m_timestamp.clear();
+ m_bHasThumbnail = false;
+}
+
+CSavestate::CSavestate(const CVariant &object)
+{
+ Deserialize(object);
+}
+
+const CSavestate& CSavestate::operator=(const CSavestate& rhs)
+{
+ if (this != &rhs)
+ {
+ m_path = rhs.m_path;
+ m_databaseId = rhs.m_databaseId;
+ m_bAutoSave = rhs.m_bAutoSave;
+ m_slot = rhs.m_slot;
+ m_label = rhs.m_label;
+ m_size = rhs.m_size;
+ m_gamePath = rhs.m_gamePath;
+ m_gameClient = rhs.m_gameClient;
+ m_gameCRC = rhs.m_gameCRC;
+ m_playtimeFrames = rhs.m_playtimeFrames;
+ m_playtimeWallClock = rhs.m_playtimeWallClock;
+ m_timestamp = rhs.m_timestamp;
+ m_bHasThumbnail = rhs.m_bHasThumbnail;
+ }
+ return *this;
+}
+
+bool CSavestate::Read(std::vector<uint8_t> &data)
+{
+ CStdString path(GetPath());
+ CLog::Log(LOGDEBUG, "CSavestate: Reading \"%s\"", path.c_str());
+ if (path.empty())
+ return false;
+
+ CFile file;
+ if (file.Open(path) && file.GetLength() > 0)
+ {
+ data.resize((size_t)file.GetLength());
+ file.Read(data.data(), data.size());
+ return true;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "CSavestate: Can't read \"%s\"", path.c_str());
+ }
+ return false;
+}
+
+bool CSavestate::Write(const std::vector<uint8_t> &data, bool bThumbnail /* = true */)
+{
+ CStdString path(GetPath());
+ m_bHasThumbnail = false;
+ CLog::Log(LOGDEBUG, "CSavestate: Writing \"%s\"", path.c_str());
+ if (path.empty())
+ return false;
+
+ SetTimestamp();
+
+ CFile file;
+ if (!file.OpenForWrite(path, true) || file.Write(data.data(), data.size()) != (int)data.size())
+ {
+ CLog::Log(LOGERROR, "CSavestate: Error writing \"%s\"", path.c_str());
+ return false;
+ }
+
+ if (bThumbnail)
+ {
+ // Create thumbnail image
+ CStdString strThumb(GetThumbnail());
+ float aspectRatio = g_renderManager.GetAspectRatio();
+
+ unsigned int width = SAVESTATE_THUMB_WIDTH;
+ unsigned int height = (unsigned int)(SAVESTATE_THUMB_WIDTH / aspectRatio);
+ if (height > SAVESTATE_THUMB_WIDTH)
+ {
+ height = SAVESTATE_THUMB_WIDTH;
+ width = (unsigned int)(SAVESTATE_THUMB_WIDTH * aspectRatio);
+ }
+
+ CRenderCapture *thumbnail = g_renderManager.AllocRenderCapture();
+ if (thumbnail)
+ {
+ g_renderManager.Capture(thumbnail, width, height, CAPTUREFLAG_IMMEDIATELY);
+
+ // If we are running off-thread (such as auto-saving from the player thread),
+ // Capture() will return immediately and queue the capture job, causing this
+ // to fail. If off-thread thumb creation is desired, it will have to be
+ // handled differently.
+ if (thumbnail->GetUserState() == CAPTURESTATE_DONE)
+ {
+ if (CPicture::CreateThumbnailFromSurface(thumbnail->GetPixels(), width, height, thumbnail->GetWidth() * 4, strThumb))
+ m_bHasThumbnail = true;
+ }
+ else
+ {
+ if (g_application.IsCurrentThread())
+ CLog::Log(LOGERROR, "CSavestate: failed to capture thumbnail in app thread");
+ else
+ CLog::Log(LOGDEBUG, "CSavestate: failed to capture thumbnail");
+ }
+
+ g_renderManager.ReleaseRenderCapture(thumbnail);
+ }
+ }
+
+ return true;
+}
+
+bool CSavestate::Rename(const CStdString &newLabel)
+{
+ CStdString oldPath(GetPath());
+ SetSaveTypeLabel(newLabel);
+ CStdString newPath(GetPath());
+ CLog::Log(LOGDEBUG, "CSavestate: Renaming \"%s\"", oldPath.c_str());
+ CLog::Log(LOGDEBUG, "CSavestate: to \"%s\"", newPath.c_str());
+ if (oldPath.empty() || newPath.empty())
+ return false;
+
+ bool success;
+ if (!(success = CFile::Rename(oldPath, newPath)))
+ CLog::Log(LOGERROR, "CSavestate: Error renaming save state");
+
+ if (success && m_bHasThumbnail)
+ {
+ CStdString oldThumbnail = URIUtils::ReplaceExtension(oldPath, ".png");
+ CStdString newThumbnail = URIUtils::ReplaceExtension(newPath, ".png");
+ if (CFile::Rename(oldThumbnail, newThumbnail))
+ CLog::Log(LOGDEBUG, "CSavestate: Renamed save state thumbnail");
+ else
+ CLog::Log(LOGDEBUG, "CSavestate: Error renaming save state thumbnail");
+ }
+
+ return success;
+}
+
+bool CSavestate::Delete()
+{
+ CStdString path(GetPath());
+ CLog::Log(LOGDEBUG, "CSavestate: Deleting \"%s\"", path.c_str());
+ if (path.empty())
+ return false;
+
+ bool success; // Success is determined by primary file deletion
+ if (!(success = CFile::Delete(path)))
+ CLog::Log(LOGERROR, "CSavestate: Can't delete \"%s\"", path.c_str());
+
+ CStdString thumbPath(GetThumbnail());
+ CLog::Log(LOGDEBUG, "CSavestate: Deleting thumbnail \"%s\"", thumbPath.c_str());
+ if (!CFile::Delete(thumbPath))
+ CLog::Log(LOGDEBUG, "CSavestate: Can't delete \"%s\"", thumbPath.c_str());
+
+ return success;
+}
+
+const CStdString &CSavestate::GetPath() const
+{
+ if (m_path.empty() && !m_gameClient.empty() && !m_gameCRC.empty())
+ {
+ CStdString hash;
+ if (m_bAutoSave)
+ hash.Format("%s/%s", m_gameClient.c_str(), m_gameCRC.c_str());
+ else if (m_slot)
+ hash.Format("%s/%s_%u", m_gameClient.c_str(), m_gameCRC.c_str(), m_slot);
+ else
+ {
+ Crc32 crc;
+ crc.Compute(m_label);
+ hash.Format("%s/%s_%08x", m_gameClient.c_str(), m_gameCRC.c_str(), (unsigned __int32)crc);
+ }
+ hash += SAVESTATE_EXTENSION;
+ URIUtils::AddFileToFolder(g_settings.GetSavegamesFolder(), hash, m_path);
+ }
+ return m_path;
+}
+
+void CSavestate::SetPath(const CStdString &path)
+{
+ Reset();
+
+ if (!URIUtils::GetExtension(path).Equals(SAVESTATE_EXTENSION))
+ return;
+
+ // Analyze the save state name to determine the type
+ CStdString saveName = URIUtils::GetFileName(path);
+ if (saveName.size() == 8 + 10) // .savestate is 10 characters
+ {
+ // Auto-save (file name is like feba62c2.savestate)
+ SetSaveTypeAuto();
+ }
+ else if (saveName.size() == 8 + 2 + 10 && '1' <= saveName[9] && saveName[9] <= '9')
+ {
+ // Save type slot (file name is like feba62c2_1.savestate)
+ unsigned int slot = saveName[9] - '0';
+ SetSaveTypeSlot(slot);
+ }
+ else if (saveName.size() == 8 + 1 + 8 + 10)
+ {
+ // Save type label (file name is like feba62c2_8dc22669.savestate)
+ SetSaveTypeLabel(""); // Unknown label for now
+ }
+ else
+ {
+ return; // Invalid
+ }
+
+ // Game CRC is first 8 characters of save state file name
+ m_gameCRC = saveName.substr(0, 8);
+
+ // Game client ID is parent folder
+ CStdString gameclientId = URIUtils::GetParentPath(path);
+ URIUtils::RemoveSlashAtEnd(gameclientId);
+ m_gameClient = URIUtils::GetFileName(gameclientId);
+
+ m_path = path;
+}
+
+CStdString CSavestate::GetThumbnail() const
+{
+ CStdString path(GetPath());
+ if (path.length() > strlen(SAVESTATE_EXTENSION))
+ path = URIUtils::ReplaceExtension(path, ".png");
+ return path;
+}
+
+void CSavestate::SetSaveTypeAuto()
+{
+ if (GetSaveType() != SAVETYPE_AUTO)
+ m_path.clear();
+
+ m_bAutoSave = true;
+ m_slot = 0;
+ // Autosave on %s
+ m_label.Format(g_localizeStrings.Get(15026).c_str(), CDateTime::GetCurrentDateTime().GetAsLocalizedDateTime().c_str());
+}
+
+void CSavestate::SetSaveTypeSlot(unsigned int slot)
+{
+ if (GetSaveType() != SAVETYPE_SLOT || m_slot != slot)
+ m_path.clear();
+
+ m_bAutoSave = false;
+ m_slot = slot;
+ // Slot %d
+ m_label.Format(g_localizeStrings.Get(15027).c_str(), slot);
+}
+
+void CSavestate::SetSaveTypeLabel(const CStdString &label)
+{
+ m_path.clear();
+
+ m_bAutoSave = false;
+ m_slot = 0;
+ m_label = label;
+}
+
+void CSavestate::SetGameCRCFromFile(const CStdString &filename)
+{
+ std::vector<char> buffer;
+ CFile file;
+ int64_t length;
+ if (file.Open(filename) && (length = file.GetLength()) > 0)
+ {
+ buffer.resize((size_t)length);
+ file.Read(buffer.data(), length);
+ SetGameCRCFromFile(buffer.data(), buffer.size());
+ }
+}
+
+void CSavestate::SetGameCRCFromFile(const char *data, size_t length)
+{
+ Crc32 crc;
+ crc.Compute(data, length);
+ m_gameCRC.Format("%08x", (unsigned __int32)crc);
+}
+
+void CSavestate::SetTimestamp(const CStdString &strTimestamp /* = empty */)
+{
+ if (!strTimestamp.empty())
+ m_timestamp = strTimestamp;
+ else
+ m_timestamp = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
+}
+
+void CSavestate::Serialize(CVariant& value) const
+{
+ value["path"] = GetPath();
+ value["databaseid"] = m_databaseId;
+ value["autosave"] = m_bAutoSave;
+ value["slot"] = m_slot;
+ value["label"] = m_label;
+ value["size"] = m_size;
+ value["gamepath"] = m_gamePath;
+ value["gameclient"] = m_gameClient;
+ value["gamecrc"] = m_gameCRC;
+ value["playtimeframes"] = m_playtimeFrames;
+ value["playtimewallclock"] = m_playtimeWallClock;
+ value["timestamp"] = m_timestamp;
+ value["hasthumbnail"] = m_bHasThumbnail;
+}
+
+void CSavestate::Deserialize(const CVariant& value)
+{
+ m_path = value["path"].asString();
+ m_databaseId = (int)value["databaseid"].asInteger();
+ m_bAutoSave = value["autosave"].asBoolean();
+ m_slot = (unsigned int)value["slot"].asUnsignedInteger();
+ m_label = value["label"].asString();
+ m_size = (size_t)value["size"].asUnsignedInteger();
+ m_gamePath = value["gamepath"].asString();
+ m_gameClient = value["gameclient"].asString();
+ m_gameCRC = value["gamecrc"].asString();
+ m_playtimeFrames = value["playtimeframes"].asUnsignedInteger();
+ m_playtimeWallClock = value["playtimewallclock"].asDouble();
+ m_timestamp = value["timestamp"].asString();
+ m_bHasThumbnail = value["hasthumbnail"].asBoolean();
+}
View
133 xbmc/games/savegames/Savestate.h
@@ -0,0 +1,133 @@
+#pragma once
+/*
+ * Copyright (C) 2005-2013 Team XBMC
+ * http://www.xbmc.org
+ *
+ * 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 XBMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "utils/IDeserializable.h"
+#include "utils/ISerializable.h"
+#include "utils/StdString.h"
+
+#include <stdint.h>
+#include <vector>
+
+class CDateTime;
+
+class CSavestate : public ISerializable, public IDeserializable
+{
+public:
+ enum SAVETYPE
+ {
+ SAVETYPE_AUTO,
+ SAVETYPE_SLOT,
+ SAVETYPE_LABEL,
+ };
+
+ CSavestate() { Reset(); }
+ CSavestate(const CVariant &object);
+ void Reset();
+ CSavestate(const CSavestate &other) { *this = other; }
+ const CSavestate& operator=(const CSavestate &rhs);
+
+ bool Read(std::vector<uint8_t> &data);
+ bool Write(const std::vector<uint8_t> &data, bool bThumbnail = true);
+ bool Rename(const CStdString &newLabel);
+ bool Delete();
+
+ virtual void Serialize(CVariant& value) const;
+ virtual void Deserialize(const CVariant& value);
+
+ // Serializable information
+
+ /**
+ * Path to savestate is derived from game client and game CRC. Returns empty
+ * if either of these is unknown. Format is
+ *
+ * Autosave (hex is game CRC):
+ * special://savegames/gameclient.id/feba62c2.savestate
+ *
+ * Save type slot (digit after the underscore is slot 1-9):
+ * special://savegames/gameclient.id/feba62c2_1.savestate
+ *
+ * Save type label (hex after the underscore is CRC of the label):
+ * special://savegames/gameclient.id/feba62c2_8dc22669.savestate
+ */
+ const CStdString &GetPath() const;
+ void SetPath(const CStdString &path);
+
+ CStdString GetThumbnail() const;
+
+ int GetDatabaseId() const { return m_databaseId; }
+ void SetDatabaseId(int databaseId) { m_databaseId = databaseId; }
+ bool IsDatabaseObject() const { return m_databaseId != -1; }
+
+ SAVETYPE GetSaveType() const { return m_bAutoSave ? SAVETYPE_AUTO : m_slot ? SAVETYPE_SLOT : SAVETYPE_LABEL; }
+ void SetSaveTypeAuto();
+
+ unsigned int GetSlot() const { return m_slot; }
+ void SetSaveTypeSlot(unsigned int slot);
+
+ const CStdString &GetLabel() const { return m_label; }
+ void SetSaveTypeLabel(const CStdString &label);
+
+ // Excluding header (if XBMC savestate header is written with the file in the future)
+ size_t GetSize() const { return m_size; }
+ void SetSize(size_t size) { m_size = size; }
+
+ const CStdString &GetGamePath() const { return m_gamePath; }
+ void SetGamePath(const CStdString &gamePath) { m_gamePath = gamePath; }
+
+ const CStdString &GetGameClient() const { return m_gameClient; }
+ void SetGameClient(const CStdString &gameClient) { m_gameClient = gameClient; }
+
+ const CStdString &GetGameCRC() const { return m_gameCRC; }
+ void SetGameCRC(const CStdString &gameCRC) { m_gameCRC = gameCRC; }
+ void SetGameCRCFromFile(const CStdString &filename);
+ void SetGameCRCFromFile(const char *data, size_t length);
+
+ uint64_t GetPlaytimeFrames() const { return m_playtimeFrames; }
+ void SetPlaytimeFrames(uint64_t playtimeFrames) { m_playtimeFrames = playtimeFrames; }
+
+ double GetPlaytimeWallClock() const { return m_playtimeWallClock; } // seconds
+ void SetPlaytimeWallClock(double playtimeWallClock) { m_playtimeWallClock = playtimeWallClock; } // seconds
+
+ const CStdString &GetTimestamp() const { return m_timestamp; } // DB datetime
+ void SetTimestamp(const CStdString &strTimestamp = ""); // Sets current time if empty
+
+ double HasThumbnail() const { return m_bHasThumbnail; }
+ void SetHasThumbnail(bool hasThumbnail) { m_bHasThumbnail = hasThumbnail; }
+
+private:
+ // If we consider writing header information to ROM files (using BSON most favorably)
+ static const unsigned int SAVESTATE_VERSION = 1;
+
+ mutable CStdString m_path;
+
+ int m_databaseId;
+ bool m_bAutoSave;
+ unsigned int m_slot;
+ CStdString m_label;
+ size_t m_size; // excluding XBMC header
+ CStdString m_gamePath;
+ CStdString m_gameClient;
+ CStdString m_gameCRC;
+ uint64_t m_playtimeFrames;
+ double m_playtimeWallClock; // seconds
+ CStdString m_timestamp;
+ bool m_bHasThumbnail;
+};
View
279 xbmc/games/savegames/SavestateDatabase.cpp
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2012 Team XBMC
+ * http://www.xbmc.org
+ *
+ * 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 XBMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#define USE_BSON
+
+#include "SavestateDatabase.h"
+#include "FileItem.h"
+#include "settings/AdvancedSettings.h"
+#ifdef USE_BSON
+#include "utils/BSONVariantParser.h"
+#include "utils/BSONVariantWriter.h"
+#else
+#include "utils/JSONVariantParser.h"
+#include "utils/JSONVariantWriter.h"
+#endif
+#include "utils/log.h"
+#include "utils/Variant.h"
+#include "XBDateTime.h"
+
+using namespace std;
+using namespace XFILE;
+
+CSavestateDatabase::CSavestateDatabase() : CDynamicDatabase("savestate")
+{
+ BeginDeclarations();
+ DeclareIndex("path", "VARCHAR(512)");
+ DeclareOneToMany("gamepath", "VARCHAR(512)");
+ DeclareOneToMany("gamecrc", "CHAR(8)");
+ DeclareOneToMany("gameclient", "VARCHAR(64)");
+}
+
+bool CSavestateDatabase::Open()
+{
+ return CDynamicDatabase::Open(g_advancedSettings.m_databaseSavestates);
+}
+
+bool CSavestateDatabase::CreateTables()
+{
+ try
+ {
+ BeginTransaction();
+ if (!CDynamicDatabase::CreateTables())
+ return false;
+
+ CommitTransaction();
+ return true;
+ }
+ catch (dbiplus::DbErrors&)
+ {
+ CLog::Log(LOGERROR, "SavestateDatabase: unable to create tables (error %i)", (int)GetLastError());
+ RollbackTransaction();
+ }
+ return false;
+}
+
+bool CSavestateDatabase::UpdateOldVersion(int version)
+{
+ if (version < 1)
+ {
+ BeginDeclarations();
+ DeclareIndex("path", "VARCHAR(512)");
+ DeclareOneToMany("gamepath", "VARCHAR(512)");
+ DeclareOneToMany("gamecrc", "CHAR(8)");
+ DeclareOneToMany("gameclient", "CHAR(8)");
+ }
+ return true;
+}
+
+bool CSavestateDatabase::Exists(const CVariant &object, int &idObject)
+{
+ if (!IsValid(object))
+ return false;
+
+ CStdString strSQL = PrepareSQL(
+ "SELECT savestate.idsavestate "
+ "FROM savestate "
+ "WHERE path='%s'",
+ object["path"].asString().c_str()
+ );
+
+ if (m_pDS->query(strSQL.c_str()))
+ {
+ bool bFound = false;
+ if (m_pDS->num_rows() != 0)
+ {
+ idObject = m_pDS->fv(0).get_asInt();
+ bFound = true;
+ }
+ m_pDS->close();
+ return bFound;
+ }
+
+ return false;
+}
+
+bool CSavestateDatabase::IsValid(const CVariant &object) const
+{
+ return !object["path"].asString().empty();
+}
+
+CFileItem* CSavestateDatabase::CreateFileItem(const CVariant &object, int id) const
+{
+ CSavestate p(object);
+ CFileItem *item = new CFileItem(p.GetLabel());
+
+ item->SetPath(p.GetPath());
+ if (p.HasThumbnail())
+ item->SetArt("thumb", p.GetThumbnail());
+ else
+ item->SetArt("thumb", "DefaultHardDisk.png");
+
+ // Use the slot number as the second label (or blank if a non-slot save type)
+ if (p.GetSaveType() == CSavestate::SAVETYPE_SLOT)
+ {
+ CStdString strSlot;
+ strSlot.Format("%u", p.GetSlot());
+ item->SetLabel2(strSlot);
+ }
+
+ CDateTime timestamp;
+ timestamp.SetFromDBDateTime(p.GetTimestamp());
+ item->m_dateTime = timestamp;
+
+ // Provide the "duration" property for CGUIInfoManager::GetItemLabel() to
+ // determine the duration of the file item.
+ item->SetProperty("duration", (uint64_t)p.GetPlaytimeWallClock());
+ item->SetProperty("gameclient", p.GetGameClient());
+
+ item->m_dwSize = p.GetSize();
+
+ item->m_bIsFolder = false;
+ return item;
+}
+
+bool CSavestateDatabase::GetCRC(const CStdString &gamePath, CStdString &strCrc)
+{
+ if (NULL == m_pDB.get()) return false;
+ if (NULL == m_pDS.get()) return false;
+
+ CStdString strSQL;
+
+ try
+ {
+ strSQL = PrepareSQL(
+ "SELECT gamecrc "
+ "FROM savestate "
+ "JOIN gamepath ON savestate.idgamepath=gamepath.idgamepath "
+ "JOIN gamecrc ON savestate.idgamecrc=gamecrc.idgamecrc "
+ "WHERE gamepath='%s' "
+ "LIMIT 1",
+ gamePath.c_str()
+ );
+ if (m_pDS->query(strSQL.c_str()))
+ {
+ if (m_pDS->num_rows() != 0)
+ {
+ strCrc = m_pDS->fv(0).get_asString();
+ m_pDS->close();
+ return !strCrc.empty();
+ }
+ m_pDS->close();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "%s - Unable to get CRC. SQL: %s", __FUNCTION__, strSQL.c_str());
+ }
+ return false;
+}
+
+bool CSavestateDatabase::GetAutoSaveByPath(const CStdString &gameClient, const CStdString &gamePath, CSavestate &savestate)
+{
+ return GetAutoSave(gameClient, true, gamePath, savestate);
+}
+
+bool CSavestateDatabase::GetAutoSaveByCrc(const CStdString &gameClient, const CStdString &gameCrc, CSavestate &savestate)
+{
+ return GetAutoSave(gameClient, false, gameCrc, savestate);
+}
+
+bool CSavestateDatabase::GetAutoSave(const CStdString &gameClient, bool usePath, const CStdString &value, CSavestate &savestate)
+{
+ if (NULL == m_pDB.get()) return false;
+ if (NULL == m_pDS.get()) return false;
+
+ CStdString strSQL;
+
+ try
+ {
+ strSQL = PrepareSQL(
+ "SELECT idsavestate, strContentBSON64 "
+ "FROM savestate "
+ "JOIN gameclient ON savestate.idgameclient=gameclient.idgameclient "
+ "JOIN gamepath ON savestate.idgamepath=gamepath.idgamepath "
+ "WHERE gameclient='%s' AND %s='%s'",
+ gameClient.c_str(), usePath ? "gamepath" : "gamecrc", value.c_str()
+ );
+ if (m_pDS->query(strSQL.c_str()))
+ {
+ while (!m_pDS->eof())
+ {
+#ifdef USE_BSON
+ CVariant var = CBSONVariantParser::ParseBase64(m_pDS->fv(1).get_asString());
+#else
+ const unsigned char *output = reinterpret_cast<const unsigned char*>(m_pDS->fv(1).get_asString().c_str());
+ CVariant var = CJSONVariantParser::Parse(output, m_pDS->fv(1).get_asString().size());
+#endif
+ if (var["autosave"].asBoolean())
+ {
+ savestate.Deserialize(var);
+ savestate.SetDatabaseId(m_pDS->fv(0).get_asInt());
+ return true;
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "%s - Unable to enumerate objects. SQL: %s", __FUNCTION__, strSQL.c_str());
+ }
+ return false;
+}
+
+bool CSavestateDatabase::Save(CSavestate &savestate, const std::vector<uint8_t> &data)
+{
+ savestate.SetSize(data.size());
+ if (Open() && savestate.Write(data))
+ {
+ if (AddObject(&savestate) != -1)
+ return true;
+ else
+ CLog::Log(LOGERROR, "CSavestateDatabase: Failed to update the database with save state information");
+ }
+ return false;
+}
+
+bool CSavestateDatabase::RenameSaveState(const CStdString &saveStatePath, const CStdString &newLabel)
+{
+ CSavestate savestate;
+ if (Open() && GetObjectByIndex("path", saveStatePath.c_str(), &savestate) && savestate.GetLabel() != newLabel)
+ {
+ if (savestate.Rename(newLabel))
+ return AddObject(&savestate) != -1;
+ }
+ return false;
+}
+
+bool CSavestateDatabase::DeleteSaveState(const CStdString &saveStatePath, bool deleteOrphans /* = true */)
+{
+ CSavestate savestate;
+ savestate.SetPath(saveStatePath);
+ if (Open() && !DeleteObjectByIndex("path", saveStatePath.c_str(), deleteOrphans))
+ {
+ // Failed to delete the object. If it still exists, don't delete the save
+ // state from the file system, as that would cause inconsistencies
+ if (GetObjectByIndex("path", saveStatePath.c_str(), &savestate))
+ return false;
+ }
+ return savestate.Delete();
+}
View
61 xbmc/games/savegames/SavestateDatabase.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012 Team XBMC
+ * http://www.xbmc.org
+ *
+ * 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 XBMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+#pragma once
+
+#include "dbwrappers/DynamicDatabase.h"
+#include "Savestate.h"
+#include "utils/StdString.h"
+
+class CSavestateDatabase : public CDynamicDatabase
+{
+public:
+ CSavestateDatabase();
+ virtual ~CSavestateDatabase() { }
+
+ virtual bool Open();
+
+ bool GetCRC(const CStdString &gamePath, CStdString &strCrc);
+
+ bool GetAutoSaveByPath(const CStdString &gameClient, const CStdString &gamePath, CSavestate &savestate);
+ bool GetAutoSaveByCrc(const CStdString &gameClient, const CStdString &gameCrc, CSavestate &savestate);
+
+ bool Save(CSavestate &savestate, const std::vector<uint8_t> &data);
+ bool RenameSaveState(const CStdString &saveStatePath, const CStdString &newLabel);
+ bool DeleteSaveState(const CStdString &saveStatePath, bool deleteOrphans = true);
+
+protected:
+ virtual int GetMinVersion() const { return 1; }
+ virtual const char *GetBaseDBName() const { return "MySavestates"; }
+
+ virtual bool CreateTables();
+ virtual bool UpdateOldVersion(int version);
+
+ /*!
+ * Uniqueness is quantified by path
+ * @throw dbiplus::DbErrors
+ */
+ virtual bool Exists(const CVariant &object, int &idObject);
+ virtual bool IsValid(const CVariant &object) const;
+
+ virtual CFileItem *CreateFileItem(const CVariant &object, int id) const;
+
+private:
+ bool GetAutoSave(const CStdString &gameClient, bool usePath, const CStdString &value, CSavestate &savestate);
+};
View
22 xbmc/guilib/Key.h
@@ -355,6 +355,28 @@
#define ACTION_LIGHTGUN_START 330
#define ACTION_GAME_CONTROL_END 330
+#define ACTION_SAVE 380
+#define ACTION_SAVE1 381
+#define ACTION_SAVE2 382
+#define ACTION_SAVE3 383
+#define ACTION_SAVE4 384
+#define ACTION_SAVE5 385
+#define ACTION_SAVE6 386
+#define ACTION_SAVE7 387
+#define ACTION_SAVE8 388
+#define ACTION_SAVE9 389
+
+#define ACTION_LOAD 390
+#define ACTION_LOAD1 391
+#define ACTION_LOAD2 392
+#define ACTION_LOAD3 393
+#define ACTION_LOAD4 394
+#define ACTION_LOAD5 395
+#define ACTION_LOAD6 396
+#define ACTION_LOAD7 397
+#define ACTION_LOAD8 398
+#define ACTION_LOAD9 399
+
// touch actions
#define ACTION_TOUCH_TAP 401
View
21 xbmc/input/ButtonTranslator.cpp
@@ -270,6 +270,27 @@ static const ActionMapping actions[] =
{"lightgunpause" , ACTION_LIGHTGUN_PAUSE},
{"lightgunstart" , ACTION_LIGHTGUN_START},
+ {"save" , ACTION_SAVE},
+ {"save1" , ACTION_SAVE1},
+ {"save2" , ACTION_SAVE2},
+ {"save3" , ACTION_SAVE3},
+ {"save4" , ACTION_SAVE4},
+ {"save5" , ACTION_SAVE5},
+ {"save6" , ACTION_SAVE6},
+ {"save7" , ACTION_SAVE7},
+ {"save8" , ACTION_SAVE8},
+ {"save9" , ACTION_SAVE9},
+ {"load" , ACTION_LOAD},
+ {"load1" , ACTION_LOAD1},
+ {"load2" , ACTION_LOAD2},
+ {"load3" , ACTION_LOAD3},
+ {"load4" , ACTION_LOAD4},
+ {"load5" , ACTION_LOAD5},
+ {"load6" , ACTION_LOAD6},
+ {"load7" , ACTION_LOAD7},
+ {"load8" , ACTION_LOAD8},
+ {"load9" , ACTION_LOAD9},
+
// Touch
{"tap" , ACTION_TOUCH_TAP},
{"longpress" , ACTION_TOUCH_LONGPRESS},
View
12 xbmc/settings/AdvancedSettings.cpp
@@ -320,6 +320,7 @@ void CAdvancedSettings::Initialize()
m_databaseMusic.Reset();
m_databaseVideo.Reset();
+ m_databaseSavestates.Reset();
m_logLevelHint = m_logLevel = LOG_LEVEL_NORMAL;
}
@@ -1029,6 +1030,17 @@ void CAdvancedSettings::ParseSettingsFile(const CStdString &file)
XMLUtils::GetString(pDatabase, "pass", m_databaseEpg.pass);
XMLUtils::GetString(pDatabase, "name", m_databaseEpg.name);
}
+
+ pDatabase = pRootElement->FirstChildElement("SavestateDatabase");
+ if (pDatabase)
+ {
+ XMLUtils::GetString(pDatabase, "type", m_databaseSavestates.type);
+ XMLUtils::GetString(pDatabase, "host", m_databaseSavestates.host);
+ XMLUtils::GetString(pDatabase, "port", m_databaseSavestates.port);
+ XMLUtils::GetString(pDatabase, "user", m_databaseSavestates.user);
+ XMLUtils::GetString(pDatabase, "pass", m_databaseSavestates.pass);
+ XMLUtils::GetString(pDatabase, "name", m_databaseSavestates.name);
+ }
pElement = pRootElement->FirstChildElement("enablemultimediakeys");
if (pElement)
View
1 xbmc/settings/AdvancedSettings.h
@@ -342,6 +342,7 @@ class CAdvancedSettings
DatabaseSettings m_databaseVideo; // advanced video database setup
DatabaseSettings m_databaseTV; // advanced tv database setup
DatabaseSettings m_databaseEpg; /*!< advanced EPG database setup */
+ DatabaseSettings m_databaseSavestates; // advanced savegame database setup
bool m_bPreferVFS; // Prefer using XBMC to load files if the emulator supports it (~50% do)
bool m_bAllowZip; // ~50% say they load .zips, but some crash. If the emulator allows XBMC to
View
2 xbmc/settings/GUISettings.cpp
@@ -1015,6 +1015,8 @@ void CGUISettings::Initialize()
AddInt(gamesGen, "games.rewindtime", 15022, 60, 10, 10, 600, SPIN_CONTROL_INT_PLUS, MASK_SECS); // Maximum rewind time
// Audio delay (ms), lower values might cause buffer underruns
AddInt(NULL, "games.audiodelay", 15017, 500, 0, 50, 1000, SPIN_CONTROL_INT_PLUS, MASK_MS); // Audio delay
+ AddBool(gamesGen, "games.savestates", 15024, true); // Automatically load previous game state if supported
+ AddBool(gamesGen, "games.autosave", 15025, true); // Save game state every 30 seconds
AddSeparator(gamesGen, "games.sep1");
AddString(gamesGen, "games.manageaddons", 24025, "", BUTTON_CONTROL_STANDARD); // Manage emulators...
CSettingsCategory* gamesDebug = AddCategory(SETTINGS_GAMES, "gamesdebug", 14092); // Debugging
View
12 xbmc/settings/Settings.cpp
@@ -1703,6 +1703,17 @@ CStdString CSettings::GetBookmarksThumbFolder() const
return folder;
}
+CStdString CSettings::GetSavegamesFolder() const
+{
+ CStdString folder;
+ if (GetCurrentProfile().hasDatabases())
+ URIUtils::AddFileToFolder(GetProfileUserDataFolder(), "Savegames", folder);
+ else
+ URIUtils::AddFileToFolder(GetUserDataFolder(), "Savegames", folder);
+
+ return folder;
+}
+
CStdString CSettings::GetLibraryFolder() const
{
CStdString folder;
@@ -1744,6 +1755,7 @@ void CSettings::CreateProfileFolders()
CDirectory::Create(GetThumbnailsFolder());
CDirectory::Create(GetVideoThumbFolder());
CDirectory::Create(GetBookmarksThumbFolder());
+ CDirectory::Create(GetSavegamesFolder());
CLog::Log(LOGINFO, "thumbnails folder: %s", GetThumbnailsFolder().c_str());
for (unsigned int hex=0; hex < 16; hex++)
{
View
1 xbmc/settings/Settings.h
@@ -330,6 +330,7 @@ class CSettings
CStdString GetVideoThumbFolder() const;
CStdString GetBookmarksThumbFolder() const;
CStdString GetLibraryFolder() const;
+ CStdString GetSavegamesFolder() const;
CStdString GetSourcesFile() const;
CStdString GetSettingsFile() const;
View
6 xbmc/settings/windows/GUIWindowSettingsCategory.cpp
@@ -999,6 +999,12 @@ void CGUIWindowSettingsCategory::UpdateSettings()
if (pControl)
pControl->SetEnabled(CPeripheralImon::GetCountOfImonsConflictWithDInput() == 0);
}
+ else if (strSetting.Equals("games.autosave"))
+ {
+ CGUIControl *pControl = (CGUIControl *)GetControl(pSettingControl->GetID());
+ if (pControl)
+ pControl->SetEnabled(g_guiSettings.GetBool("games.savestates"));
+ }
else if (strSetting.Equals("games.rewindtime"))
{
CGUIControl *pControl = (CGUIControl *)GetControl(pSettingControl->GetID());

0 comments on commit 3b01427

Please sign in to comment.