aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGarrett Brown <themagnificentmrb@gmail.com>2018-07-05 20:06:48 -0700
committerGarrett Brown <themagnificentmrb@gmail.com>2018-08-14 11:29:15 -0700
commit3ed099ae0d9ea153f641d00e4c627ba8c8715a24 (patch)
tree5cd347caf854ead53bfde225ffee86ebb32c013c
parentcd50c4f4219773548d839a94feaf57d8c6dff3b9 (diff)
RetroPlayer: Merge savestate metadata into .sav file
This removes the auxiliary XML metadata file used for savestates. Now, metadata is serialized into the .sav file using FlatBuffers.
-rw-r--r--cmake/messages/flatbuffers/retroplayer.txt1
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayer.cpp25
-rw-r--r--xbmc/cores/RetroPlayer/messages/CMakeLists.txt19
-rw-r--r--xbmc/cores/RetroPlayer/messages/savestate.fbs47
-rw-r--r--xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp131
-rw-r--r--xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h6
-rw-r--r--xbmc/cores/RetroPlayer/savestates/CMakeLists.txt25
-rw-r--r--xbmc/cores/RetroPlayer/savestates/ISavestate.h129
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp119
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h16
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp326
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h98
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateTypes.h (renamed from xbmc/cores/RetroPlayer/savestates/SavestateTranslator.h)18
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp11
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateUtils.h17
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectGameClient.cpp13
-rw-r--r--xbmc/guilib/guiinfo/GamesGUIInfo.cpp3
17 files changed, 810 insertions, 194 deletions
diff --git a/cmake/messages/flatbuffers/retroplayer.txt b/cmake/messages/flatbuffers/retroplayer.txt
new file mode 100644
index 0000000000..3d42d5c812
--- /dev/null
+++ b/cmake/messages/flatbuffers/retroplayer.txt
@@ -0,0 +1 @@
+xbmc/cores/RetroPlayer/messages cores/RetroPlayer/messages
diff --git a/xbmc/cores/RetroPlayer/RetroPlayer.cpp b/xbmc/cores/RetroPlayer/RetroPlayer.cpp
index b1065d1c95..87b8f047d4 100644
--- a/xbmc/cores/RetroPlayer/RetroPlayer.cpp
+++ b/xbmc/cores/RetroPlayer/RetroPlayer.cpp
@@ -18,7 +18,8 @@
#include "cores/RetroPlayer/playback/ReversiblePlayback.h"
#include "cores/RetroPlayer/process/RPProcessInfo.h"
#include "cores/RetroPlayer/rendering/RPRenderManager.h"
-#include "cores/RetroPlayer/savestates/Savestate.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
#include "cores/RetroPlayer/savestates/SavestateUtils.h"
#include "cores/RetroPlayer/streams/RPStreamManager.h"
#include "dialogs/GUIDialogYesNo.h"
@@ -42,6 +43,7 @@
#include "utils/log.h"
#include "utils/MathUtils.h"
#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
#include "FileItem.h"
#include "ServiceBroker.h"
#include "URL.h"
@@ -144,16 +146,16 @@ bool CRetroPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options
if (bSuccess && !bStandalone)
{
- std::string savestatePath = CSavestateUtils::MakeMetadataPath(fileCopy.GetPath());
+ CSavestateDatabase savestateDb;
- CSavestate save;
- if (save.Deserialize(savestatePath))
+ std::unique_ptr<ISavestate> save = savestateDb.CreateSavestate();
+ if (savestateDb.GetSavestate(fileCopy.GetPath(), *save))
{
// Check if game client is the same
- if (save.GameClient() != m_gameClient->ID())
+ if (save->GameClientID() != m_gameClient->ID())
{
ADDON::AddonPtr addon;
- if (CServiceBroker::GetAddonMgr().GetAddon(save.GameClient(), addon))
+ if (CServiceBroker::GetAddonMgr().GetAddon(save->GameClientID(), addon))
{
// Warn the user that continuing with a different game client will
// overwrite the save
@@ -555,15 +557,10 @@ void CRetroPlayer::CreatePlayback(bool bRestoreState)
const bool bStandalone = m_gameClient->GetGamePath().empty();
if (!bStandalone)
{
- std::string savestatePath = CSavestateUtils::MakeMetadataPath(m_gameClient->GetGamePath());
- if (!savestatePath.empty())
- {
- std::string redactedSavestatePath = CURL::GetRedacted(savestatePath);
- CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Loading savestate %s", redactedSavestatePath.c_str());
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Loading savestate");
- if (!SetPlayerState(savestatePath))
- CLog::Log(LOGERROR, "RetroPlayer[SAVE]: Failed to load savestate");
- }
+ if (!SetPlayerState(m_gameClient->GetGamePath()))
+ CLog::Log(LOGERROR, "RetroPlayer[SAVE]: Failed to load savestate");
}
}
diff --git a/xbmc/cores/RetroPlayer/messages/CMakeLists.txt b/xbmc/cores/RetroPlayer/messages/CMakeLists.txt
new file mode 100644
index 0000000000..e2e2ba6625
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/messages/CMakeLists.txt
@@ -0,0 +1,19 @@
+set(MESSAGES savestate.fbs)
+
+foreach(_file ${MESSAGES})
+ get_filename_component(FLATC_OUTPUT ${_file} NAME_WE)
+ set(FLATC_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${FLATC_OUTPUT}_generated.h)
+ list(APPEND FLATC_OUTPUTS ${FLATC_OUTPUT})
+
+ add_custom_command(OUTPUT ${FLATC_OUTPUT}
+ COMMAND ${FLATBUFFERS_FLATC_EXECUTABLE}
+ ARGS -c -o "${FLATBUFFERS_MESSAGES_INCLUDE_DIR}/" ${_file}
+ DEPENDS ${_file}
+ COMMENT "Building C++ header for ${_file}"
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+endforeach()
+
+add_custom_target(retroplayer_messages DEPENDS ${FLATC_OUTPUTS})
+set_target_properties(retroplayer_messages PROPERTIES FOLDER "Generated Messages"
+ INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}
+ SOURCES ${FLATC_OUTPUTS})
diff --git a/xbmc/cores/RetroPlayer/messages/savestate.fbs b/xbmc/cores/RetroPlayer/messages/savestate.fbs
new file mode 100644
index 0000000000..1d708d61f9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/messages/savestate.fbs
@@ -0,0 +1,47 @@
+//
+// Copyright (C) 2018 Team Kodi
+// This file is part of Kodi - https://kodi.tv
+//
+// SPDX-License-Identifier: MIT
+// See LICENSES/README.md for more information.
+//
+
+namespace KODI.RETRO;
+
+// Savestate schema
+// Version 1
+
+file_identifier "SAV_";
+
+enum SaveType : uint8 {
+ Unknown,
+ Auto,
+ Manual
+}
+
+table Savestate {
+ // Schema version
+ version:uint8;
+
+ // Savestate properties
+ type:SaveType;
+ slot:uint8;
+ label:string;
+ created:string; // RFC 1123 date time
+
+ // Game properties
+ game_file_name:string;
+
+ // Environment properties
+ timestamp_frames:uint64;
+ timestamp_wall_clock_ns:uint64;
+
+ // Emulator properties
+ emulator_addon_id:string;
+ emulator_version:string; // Semantic version
+
+ // Memory properties
+ memory_data:[uint8];
+}
+
+root_type Savestate;
diff --git a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp
index 71a7132040..49b4249e87 100644
--- a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp
+++ b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp
@@ -7,16 +7,15 @@
*/
#include "ReversiblePlayback.h"
-#include "cores/RetroPlayer/savestates/Savestate.h"
-#include "cores/RetroPlayer/savestates/SavestateReader.h"
-#include "cores/RetroPlayer/savestates/SavestateWriter.h"
-#include "cores/RetroPlayer/streams/memory/BasicMemoryStream.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
#include "cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.h"
#include "games/addons/GameClient.h"
#include "games/GameServices.h"
#include "games/GameSettings.h"
#include "threads/SingleLock.h"
#include "utils/MathUtils.h"
+#include "utils/URIUtils.h"
#include "ServiceBroker.h"
#include <algorithm>
@@ -29,8 +28,7 @@ using namespace RETRO;
CReversiblePlayback::CReversiblePlayback(GAME::CGameClient* gameClient, double fps, size_t serializeSize) :
m_gameClient(gameClient),
m_gameLoop(this, fps),
- m_savestateWriter(new CSavestateWriter),
- m_savestateReader(new CSavestateReader),
+ m_savestateDatabase(new CSavestateDatabase),
m_totalFrameCount(0),
m_pastFrameCount(0),
m_futureFrameCount(0),
@@ -109,106 +107,91 @@ void CReversiblePlayback::PauseAsync()
std::string CReversiblePlayback::CreateSavestate()
{
- std::string empty;
+ const size_t memorySize = m_gameClient->SerializeSize();
// Game client must support serialization
- if (m_gameClient->SerializeSize() == 0)
- return empty;
+ if (memorySize == 0)
+ return "";
- if (!m_savestateWriter->Initialize(m_gameClient, m_totalFrameCount))
- return empty;
+ //! @todo Handle savestates for standalone game clients
+ if (m_gameClient->GetGamePath().empty())
+ {
+ return "";
+ }
+
+ const CDateTime now = CDateTime::GetCurrentDateTime();
+ const std::string label = now.GetAsLocalizedDateTime();
+ const std::string gameFileName = URIUtils::GetFileName(m_gameClient->GetGamePath());
+ const uint64_t timestampFrames = m_totalFrameCount;
+ const double timestampWallClock = (m_totalFrameCount / m_gameClient->GetFrameRate()); //! @todo Accumulate playtime instead of deriving it
+ const std::string gameClientId = m_gameClient->ID();
+ const std::string gameClientVersion = m_gameClient->Version().asString();
+
+ std::unique_ptr<ISavestate> savestate = m_savestateDatabase->CreateSavestate();
+
+ savestate->SetType(SAVE_TYPE::AUTO);
+ savestate->SetLabel(label);
+ savestate->SetCreated(now);
+ savestate->SetGameFileName(gameFileName);
+ savestate->SetTimestampFrames(timestampFrames);
+ savestate->SetTimestampWallClock(timestampWallClock);
+ savestate->SetGameClientID(gameClientId);
+ savestate->SetGameClientVersion(gameClientVersion);
- std::unique_ptr<IMemoryStream> memoryStream;
- bool bHasMemoryStream = false;
+ uint8_t *memoryData = savestate->GetMemoryBuffer(memorySize);
{
CSingleLock lock(m_mutex);
- if (m_memoryStream)
+ if (m_memoryStream && m_memoryStream->CurrentFrame() != nullptr)
{
- memoryStream = std::move(m_memoryStream);
- bHasMemoryStream = true;
+ std::memcpy(memoryData, m_memoryStream->CurrentFrame(), memorySize);
}
else
{
lock.Leave();
- memoryStream.reset(new CBasicMemoryStream);
- memoryStream->Init(m_gameClient->SerializeSize(), 1);
+ if (!m_gameClient->Serialize(memoryData, memorySize))
+ return "";
}
}
- // If memory stream is empty, ask the game client for a frame
- if (memoryStream->CurrentFrame() == nullptr)
- {
- if (m_gameClient->Serialize(memoryStream->BeginFrame(), memoryStream->FrameSize()))
- memoryStream->SubmitFrame();
- }
+ savestate->Finalize();
- bool bSuccess = false;
-
- if (m_savestateWriter->WriteSave(memoryStream->CurrentFrame(), memoryStream->FrameSize()))
- {
- m_savestateWriter->WriteThumb();
+ if (!m_savestateDatabase->AddSavestate(m_gameClient->GetGamePath(), *savestate))
+ return "";
- if (m_savestateWriter->CommitToDatabase())
- bSuccess = true;
- else
- m_savestateWriter->CleanUpTransaction();
- }
-
- if (bHasMemoryStream)
- {
- CSingleLock lock(m_mutex);
- m_memoryStream = std::move(memoryStream);
- }
-
- return bSuccess ? m_savestateWriter->GetPath() : "";
+ return m_gameClient->GetGamePath();
}
bool CReversiblePlayback::LoadSavestate(const std::string& path)
{
- // Game client must support serialization
- if (m_gameClient->SerializeSize() == 0)
- return false;
+ const size_t memorySize = m_gameClient->SerializeSize();
- if (!m_savestateReader->Initialize(path, m_gameClient))
+ // Game client must support serialization
+ if (memorySize == 0)
return false;
- std::unique_ptr<IMemoryStream> memoryStream;
- bool bHasMemoryStream = false;
+ bool bSuccess = false;
+ std::unique_ptr<ISavestate> savestate = m_savestateDatabase->CreateSavestate();
+ if (m_savestateDatabase->GetSavestate(path, *savestate) && savestate->GetMemorySize() == memorySize)
{
- CSingleLock lock(m_mutex);
- if (m_memoryStream)
{
- memoryStream = std::move(m_memoryStream);
- bHasMemoryStream = true;
+ CSingleLock lock(m_mutex);
+ if (m_memoryStream)
+ {
+ m_memoryStream->SetFrameCounter(savestate->TimestampFrames());
+ std::memcpy(m_memoryStream->BeginFrame(), savestate->GetMemoryData(), memorySize);
+ m_memoryStream->SubmitFrame();
+ }
}
- else
+
+ if (m_gameClient->Deserialize(savestate->GetMemoryData(), memorySize))
{
- lock.Leave();
- memoryStream.reset(new CBasicMemoryStream);
- memoryStream->Init(m_gameClient->SerializeSize(), 1);
+ m_totalFrameCount = savestate->TimestampFrames();
+ bSuccess = true;
}
}
- bool bSuccess = false;
-
- if (m_savestateReader->ReadSave(memoryStream->BeginFrame(), memoryStream->FrameSize()))
- {
- memoryStream->SubmitFrame();
-
- m_gameClient->Deserialize(memoryStream->CurrentFrame(), memoryStream->FrameSize());
- m_totalFrameCount = m_savestateReader->GetFrameCount();
-
- bSuccess = true;
- }
-
- if (bHasMemoryStream)
- {
- CSingleLock lock(m_mutex);
- m_memoryStream = std::move(memoryStream);
- }
-
return bSuccess;
}
diff --git a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h
index 45075c8458..4e2569d6cf 100644
--- a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h
+++ b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h
@@ -26,8 +26,7 @@ namespace GAME
namespace RETRO
{
- class CSavestateReader;
- class CSavestateWriter;
+ class CSavestateDatabase;
class IMemoryStream;
class CReversiblePlayback : public IPlayback,
@@ -77,8 +76,7 @@ namespace RETRO
CCriticalSection m_mutex;
// Savestate functionality
- std::unique_ptr<CSavestateWriter> m_savestateWriter;
- std::unique_ptr<CSavestateReader> m_savestateReader;
+ std::unique_ptr<CSavestateDatabase> m_savestateDatabase;
// Playback stats
uint64_t m_totalFrameCount;
diff --git a/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt b/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt
index 5f835e5914..29e7b3177c 100644
--- a/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt
+++ b/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt
@@ -1,16 +1,21 @@
-set(SOURCES Savestate.cpp
- SavestateDatabase.cpp
- SavestateReader.cpp
- SavestateTranslator.cpp
+set(SOURCES SavestateDatabase.cpp
+ SavestateFlatBuffer.cpp
SavestateUtils.cpp
- SavestateWriter.cpp)
+)
-set(HEADERS Savestate.h
+set(HEADERS ISavestate.h
SavestateDatabase.h
- SavestateDefines.h
- SavestateReader.h
- SavestateTranslator.h
+ SavestateFlatBuffer.h
+ SavestateTypes.h
SavestateUtils.h
- SavestateWriter.h)
+)
core_add_library(retroplayer_savestates)
+
+set(DEPENDS retroplayer_messages)
+
+if(ENABLE_STATIC_LIBS)
+ add_dependencies(retroplayer_savestates ${DEPENDS})
+else()
+ add_dependencies(lib${APP_NAME_LC} ${DEPENDS})
+endif()
diff --git a/xbmc/cores/RetroPlayer/savestates/ISavestate.h b/xbmc/cores/RetroPlayer/savestates/ISavestate.h
new file mode 100644
index 0000000000..ede53b1ce1
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/ISavestate.h
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "SavestateTypes.h"
+#include "XBDateTime.h"
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace RETRO
+{
+ class ISavestate
+ {
+ public:
+ virtual ~ISavestate() = default;
+
+ /*!
+ * \brief Reset to the initial state
+ */
+ virtual void Reset() = 0;
+
+ /*!
+ * Access the data representation of this savestate
+ */
+ virtual bool Serialize(const uint8_t *&data, size_t &size) const = 0;
+
+ /// @name Savestate properties
+ ///{
+ /*!
+ * \brief The type of save action that created this savestate, either
+ * manual or automatic
+ */
+ virtual SAVE_TYPE Type() const = 0;
+
+ /*!
+ * \brief The slot this savestate was saved into, or 0 for no slot
+ *
+ * This allows for keyboard access of saved games using the number keys 1-9.
+ */
+ virtual uint8_t Slot() const = 0;
+
+ /*!
+ * \brief The label shown in the GUI for this savestate
+ */
+ virtual std::string Label() const = 0;
+
+ /*!
+ * \brief The timestamp of this savestate's creation
+ */
+ virtual CDateTime Created() const = 0;
+ ///}
+
+ /// @name Game properties
+ ///{
+ /*!
+ * \brief The name of the file beloning to this savestate's game
+ */
+ virtual std::string GameFileName() const = 0;
+ ///}
+
+ /// @name Environment properties
+ ///{
+ /*!
+ * \brief The number of frames in the entire gameplay history
+ */
+ virtual uint64_t TimestampFrames() const = 0;
+
+ /*!
+ * \brief The duration of the entire gameplay history as seen by a wall clock
+ */
+ virtual double TimestampWallClock() const = 0;
+ ///}
+
+ /// @name Game client properties
+ ///{
+ /*!
+ * \brief The game client add-on ID that created this savestate
+ */
+ virtual std::string GameClientID() const = 0;
+
+ /*!
+ * \brief The semantic version of the game client
+ */
+ virtual std::string GameClientVersion() const = 0;
+ ///}
+
+ /// @name Memory properties
+ ///{
+ /*!
+ * \brief A pointer to the internal memory (SRAM) of the frame
+ */
+ virtual const uint8_t *GetMemoryData() const = 0;
+
+ /*!
+ * \brief The size of the memory region returned by GetMemoryData()
+ */
+ virtual size_t GetMemorySize() const = 0;
+ ///}
+
+ // Build flatbuffer by setting individual fields
+ virtual void SetType(SAVE_TYPE type) = 0;
+ virtual void SetSlot(uint8_t slot) = 0;
+ virtual void SetLabel(const std::string &label) = 0;
+ virtual void SetCreated(const CDateTime &created) = 0;
+ virtual void SetGameFileName(const std::string &gameFileName) = 0;
+ virtual void SetTimestampFrames(uint64_t timestampFrames) = 0;
+ virtual void SetTimestampWallClock(double timestampWallClock) = 0;
+ virtual void SetGameClientID(const std::string &gameClient) = 0;
+ virtual void SetGameClientVersion(const std::string &gameClient) = 0;
+ virtual uint8_t *GetMemoryBuffer(size_t size) = 0;
+ virtual void Finalize() = 0;
+
+ /*!
+ * \brief Take ownership and initialize the flatbuffer with the given vector
+ */
+ virtual bool Deserialize(std::vector<uint8_t> data) = 0;
+ };
+}
+}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp
index 6b987f7ca8..45d021cdf1 100644
--- a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp
@@ -7,30 +7,92 @@
*/
#include "SavestateDatabase.h"
-#include "Savestate.h"
-#include "SavestateDefines.h"
+#include "SavestateFlatBuffer.h"
#include "SavestateUtils.h"
-#include "ServiceBroker.h"
-#include "addons/AddonManager.h"
-#include "games/GameTypes.h"
-#include "games/tags/GameInfoTag.h"
-#include "utils/StringUtils.h"
-#include "utils/Variant.h"
-#include "FileItem.h"
+#include "filesystem/File.h"
+#include "utils/log.h"
+#include "URL.h"
using namespace KODI;
using namespace RETRO;
CSavestateDatabase::CSavestateDatabase() = default;
-bool CSavestateDatabase::AddSavestate(const CSavestate& save)
+std::unique_ptr<ISavestate> CSavestateDatabase::CreateSavestate()
{
- return save.Serialize(CSavestateUtils::MakeMetadataPath(save.GamePath()));
+ std::unique_ptr<ISavestate> savestate;
+
+ savestate.reset(new CSavestateFlatBuffer);
+
+ return savestate;
+}
+
+bool CSavestateDatabase::AddSavestate(const std::string &gamePath, const ISavestate& save)
+{
+ bool bSuccess = false;
+
+ const std::string savestatePath = CSavestateUtils::MakePath(gamePath);
+
+ CLog::Log(LOGDEBUG, "Saving savestate to %s", CURL::GetRedacted(savestatePath).c_str());
+
+ const uint8_t *data = nullptr;
+ size_t size = 0;
+ if (save.Serialize(data, size))
+ {
+ XFILE::CFile file;
+ if (file.OpenForWrite(savestatePath))
+ {
+ const ssize_t written = file.Write(data, size);
+ if (written == static_cast<ssize_t>(size))
+ {
+ CLog::Log(LOGDEBUG, "Wrote savestate of %u bytes", size);
+ bSuccess = true;
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "Failed to open savestate for writing");
+ }
+
+ return bSuccess;
}
-bool CSavestateDatabase::GetSavestate(const std::string& path, CSavestate& save)
+bool CSavestateDatabase::GetSavestate(const std::string& gamePath, ISavestate& save)
{
- return save.Deserialize(path);
+ bool bSuccess = false;
+
+ const std::string savestatePath = CSavestateUtils::MakePath(gamePath);
+
+ CLog::Log(LOGDEBUG, "Loading savestate from %s", CURL::GetRedacted(savestatePath).c_str());
+
+ std::vector<uint8_t> savestateData;
+
+ XFILE::CFile savestateFile;
+ if (savestateFile.Open(savestatePath, XFILE::READ_TRUNCATED))
+ {
+ int64_t size = savestateFile.GetLength();
+ if (size > 0)
+ {
+ savestateData.resize(static_cast<size_t>(size));
+
+ const ssize_t readLength = savestateFile.Read(savestateData.data(), savestateData.size());
+ if (readLength != static_cast<ssize_t>(savestateData.size()))
+ {
+ CLog::Log(LOGERROR, "Failed to read savestate %s of size %d bytes",
+ CURL::GetRedacted(savestatePath).c_str(),
+ size);
+ savestateData.clear();
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "Failed to get savestate length: %s", CURL::GetRedacted(savestatePath).c_str());
+ }
+ else
+ CLog::Log(LOGERROR, "Failed to open savestate file %s", CURL::GetRedacted(savestatePath).c_str());
+
+ if (!savestateData.empty())
+ bSuccess = save.Deserialize(std::move(savestateData));
+
+ return bSuccess;
}
bool CSavestateDatabase::GetSavestatesNav(CFileItemList& items, const std::string& gamePath, const std::string& gameClient /* = "" */)
@@ -56,34 +118,3 @@ bool CSavestateDatabase::ClearSavestatesOfGame(const std::string& gamePath, cons
//! @todo
return false;
}
-
-CFileItem* CSavestateDatabase::CreateFileItem(const CVariant& object) const
-{
- using namespace ADDON;
-
- CSavestate save;
- save.Deserialize(object);
- CFileItem* item = new CFileItem(save.Label());
-
- item->SetPath(save.Path());
- if (!save.Thumbnail().empty())
- item->SetArt("thumb", save.Thumbnail());
- else
- {
- AddonPtr addon;
- if (CServiceBroker::GetAddonMgr().GetAddon(save.GameClient(), addon, ADDON_GAMEDLL))
- item->SetArt("thumb", addon->Icon());
- }
-
- // Use the slot number as the second label
- if (save.Type() == SAVETYPE::SLOT)
- item->SetLabel2(StringUtils::Format("%u", save.Slot()));
-
- item->m_dateTime = save.Timestamp();
- item->SetProperty(FILEITEM_PROPERTY_SAVESTATE_DURATION, static_cast<uint64_t>(save.PlaytimeWallClock()));
- item->GetGameInfoTag()->SetGameClient(save.GameClient());
- item->m_dwSize = save.Size();
- item->m_bIsFolder = false;
-
- return item;
-}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h
index 9dd5c025d0..473e3cf132 100644
--- a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h
@@ -8,19 +8,16 @@
#pragma once
+#include <memory>
#include <string>
-#define SAVESTATES_DATABASE_NAME "Savestates"
-
-class CFileItem;
class CFileItemList;
-class CVariant;
namespace KODI
{
namespace RETRO
{
- class CSavestate;
+ class ISavestate;
class CSavestateDatabase
{
@@ -28,9 +25,11 @@ namespace RETRO
CSavestateDatabase();
virtual ~CSavestateDatabase() = default;
- bool AddSavestate(const CSavestate& save);
+ std::unique_ptr<ISavestate> CreateSavestate();
+
+ bool AddSavestate(const std::string &gamePath, const ISavestate& save);
- bool GetSavestate(const std::string& path, CSavestate& save);
+ bool GetSavestate(const std::string& gamePath, ISavestate& save);
bool GetSavestatesNav(CFileItemList& items, const std::string& gamePath, const std::string& gameClient = "");
@@ -39,9 +38,6 @@ namespace RETRO
bool DeleteSavestate(const std::string& path);
bool ClearSavestatesOfGame(const std::string& gamePath, const std::string& gameClient = "");
-
- private:
- CFileItem* CreateFileItem(const CVariant& object) const;
};
}
}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp
new file mode 100644
index 0000000000..bd92f89498
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "SavestateFlatBuffer.h"
+#include "utils/log.h"
+
+#include "savestate_generated.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+namespace
+{
+ const uint8_t SCHEMA_VERSION = 1;
+
+ /*!
+ * \brief The initial size of the FlatBuffer's memory buffer
+ *
+ * 1024 is the default size in the FlatBuffers header. We might as well use
+ * this until our size requirements are more known.
+ */
+ const size_t INITIAL_FLATBUFFER_SIZE = 1024;
+
+ /*!
+ * \brief Translate the save type (RetroPlayer to FlatBuffers)
+ */
+ SaveType TranslateType(SAVE_TYPE type)
+ {
+ switch (type)
+ {
+ case SAVE_TYPE::AUTO:
+ return SaveType_Auto;
+ case SAVE_TYPE::MANUAL:
+ return SaveType_Manual;
+ default:
+ break;
+ }
+
+ return SaveType_Unknown;
+ }
+
+ /*!
+ * \brief Translate the save type (FlatBuffers to RetroPlayer)
+ */
+ SAVE_TYPE TranslateType(SaveType type)
+ {
+ switch (type)
+ {
+ case SaveType_Auto:
+ return SAVE_TYPE::AUTO;
+ case SaveType_Manual:
+ return SAVE_TYPE::MANUAL;
+ default:
+ break;
+ }
+
+ return SAVE_TYPE::UNKNOWN;
+ }
+}
+
+CSavestateFlatBuffer::CSavestateFlatBuffer()
+{
+ Reset();
+}
+
+CSavestateFlatBuffer::~CSavestateFlatBuffer() = default;
+
+void CSavestateFlatBuffer::Reset()
+{
+ m_builder.reset(new flatbuffers::FlatBufferBuilder(INITIAL_FLATBUFFER_SIZE));
+ m_data.clear();
+ m_savestate = nullptr;
+}
+
+bool CSavestateFlatBuffer::Serialize(const uint8_t *&data, size_t &size) const
+{
+ // Check if savestate was deserialized from vector or built with FlatBuffers
+ if (!m_data.empty())
+ {
+ data = m_data.data();
+ size = m_data.size();
+ }
+ else
+ {
+ data = m_builder->GetBufferPointer();
+ size = m_builder->GetSize();
+ }
+
+ return true;
+}
+
+SAVE_TYPE CSavestateFlatBuffer::Type() const
+{
+ if (m_savestate != nullptr)
+ return TranslateType(m_savestate->type());
+
+ return SAVE_TYPE::UNKNOWN;
+}
+
+void CSavestateFlatBuffer::SetType(SAVE_TYPE type)
+{
+ m_type = type;
+}
+
+uint8_t CSavestateFlatBuffer::Slot() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->slot();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetSlot(uint8_t slot)
+{
+ m_slot = slot;
+}
+
+std::string CSavestateFlatBuffer::Label() const
+{
+ std::string label;
+
+ if (m_savestate != nullptr && m_savestate->label())
+ label = m_savestate->label()->c_str();
+
+ return label;
+}
+
+void CSavestateFlatBuffer::SetLabel(const std::string &label)
+{
+ m_labelOffset.reset(new StringOffset{ m_builder->CreateString(label) });
+}
+
+CDateTime CSavestateFlatBuffer::Created() const
+{
+ CDateTime created;
+
+ if (m_savestate != nullptr && m_savestate->created())
+ created.SetFromRFC1123DateTime(m_savestate->created()->c_str());
+
+ return created;
+}
+
+void CSavestateFlatBuffer::SetCreated(const CDateTime &created)
+{
+ m_createdOffset.reset(new StringOffset{ m_builder->CreateString(created.GetAsRFC1123DateTime()) });
+}
+
+std::string CSavestateFlatBuffer::GameFileName() const
+{
+ std::string gameFileName;
+
+ if (m_savestate != nullptr && m_savestate->game_file_name())
+ gameFileName = m_savestate->game_file_name()->c_str();
+
+ return gameFileName;
+}
+
+void CSavestateFlatBuffer::SetGameFileName(const std::string &gameFileName)
+{
+ m_gameFileNameOffset.reset(new StringOffset{ m_builder->CreateString(gameFileName) });
+}
+
+uint64_t CSavestateFlatBuffer::TimestampFrames() const
+{
+ return m_savestate->timestamp_frames();
+}
+
+void CSavestateFlatBuffer::SetTimestampFrames(uint64_t timestampFrames)
+{
+ m_timestampFrames = timestampFrames;
+}
+
+double CSavestateFlatBuffer::TimestampWallClock() const
+{
+ if (m_savestate != nullptr)
+ return static_cast<double>(m_savestate->timestamp_wall_clock_ns()) / 1000.0 / 1000.0 / 1000.0;
+
+ return 0.0;
+}
+
+void CSavestateFlatBuffer::SetTimestampWallClock(double timestampWallClock)
+{
+ m_timestampWallClock = timestampWallClock;
+}
+
+std::string CSavestateFlatBuffer::GameClientID() const
+{
+ std::string gameClientId;
+
+ if (m_savestate != nullptr && m_savestate->emulator_addon_id())
+ gameClientId = m_savestate->emulator_addon_id()->c_str();
+
+ return gameClientId;
+}
+
+void CSavestateFlatBuffer::SetGameClientID(const std::string &gameClientId)
+{
+ m_emulatorAddonIdOffset.reset(new StringOffset{ m_builder->CreateString(gameClientId) });
+}
+
+std::string CSavestateFlatBuffer::GameClientVersion() const
+{
+ std::string gameClientVersion;
+
+ if (m_savestate != nullptr && m_savestate->emulator_version())
+ gameClientVersion = m_savestate->emulator_version()->c_str();
+
+ return gameClientVersion;
+}
+
+void CSavestateFlatBuffer::SetGameClientVersion(const std::string &gameClientVersion)
+{
+ m_emulatorVersionOffset.reset(new StringOffset{ m_builder->CreateString(gameClientVersion) });
+}
+
+const uint8_t *CSavestateFlatBuffer::GetMemoryData() const
+{
+ if (m_savestate != nullptr && m_savestate->memory_data())
+ return m_savestate->memory_data()->data();
+
+ return nullptr;
+}
+
+size_t CSavestateFlatBuffer::GetMemorySize() const
+{
+ if (m_savestate != nullptr && m_savestate->memory_data())
+ return m_savestate->memory_data()->size();
+
+ return 0;
+}
+
+uint8_t *CSavestateFlatBuffer::GetMemoryBuffer(size_t size)
+{
+ uint8_t *memoryBuffer = nullptr;
+
+ m_memoryDataOffset.reset(new VectorOffset{ m_builder->CreateUninitializedVector(size, &memoryBuffer) });
+
+ return memoryBuffer;
+}
+
+void CSavestateFlatBuffer::Finalize()
+{
+ // Helper class to build the nested Savestate table
+ SavestateBuilder savestateBuilder(*m_builder);
+
+ savestateBuilder.add_version(SCHEMA_VERSION);
+
+ savestateBuilder.add_type(TranslateType(m_type));
+
+ savestateBuilder.add_slot(m_slot);
+
+ if (m_labelOffset)
+ {
+ savestateBuilder.add_label(*m_labelOffset);
+ m_labelOffset.reset();
+ }
+
+ if (m_createdOffset)
+ {
+ savestateBuilder.add_created(*m_createdOffset);
+ m_createdOffset.reset();
+ }
+
+ if (m_gameFileNameOffset)
+ {
+ savestateBuilder.add_game_file_name(*m_gameFileNameOffset);
+ m_gameFileNameOffset.reset();
+ }
+
+ savestateBuilder.add_timestamp_frames(m_timestampFrames);
+
+ const uint64_t wallClockNs = static_cast<uint64_t>(m_timestampWallClock * 1000.0 * 1000.0 * 1000.0);
+ savestateBuilder.add_timestamp_wall_clock_ns(wallClockNs);
+
+ if (m_emulatorAddonIdOffset)
+ {
+ savestateBuilder.add_emulator_addon_id(*m_emulatorAddonIdOffset);
+ m_emulatorAddonIdOffset.reset();
+ }
+
+ if (m_emulatorVersionOffset)
+ {
+ savestateBuilder.add_emulator_version(*m_emulatorVersionOffset);
+ m_emulatorVersionOffset.reset();
+ }
+
+ if (m_memoryDataOffset)
+ {
+ savestateBuilder.add_memory_data(*m_memoryDataOffset);
+ m_memoryDataOffset.reset();
+ }
+
+ auto savestate = savestateBuilder.Finish();
+ FinishSavestateBuffer(*m_builder, savestate);
+
+ m_savestate = GetSavestate(m_builder->GetBufferPointer());
+}
+
+bool CSavestateFlatBuffer::Deserialize(std::vector<uint8_t> data)
+{
+ flatbuffers::Verifier verifier(data.data(), data.size());
+ if (VerifySavestateBuffer(verifier))
+ {
+ const Savestate *savestate = GetSavestate(data.data());
+
+ if (savestate->version() != SCHEMA_VERSION)
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[SAVE): Schema version %u not supported, must be version %u",
+ savestate->version(),
+ SCHEMA_VERSION);
+ }
+ else
+ {
+ m_data = std::move(data);
+ m_savestate = GetSavestate(m_data.data());
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h
new file mode 100644
index 0000000000..b59b08ce93
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "ISavestate.h"
+
+#include <flatbuffers/flatbuffers.h>
+
+#include <memory>
+
+namespace flatbuffers
+{
+ class FlatBufferBuilder;
+}
+
+namespace KODI
+{
+namespace RETRO
+{
+ struct Savestate;
+ struct SavestateBuilder;
+
+ class CSavestateFlatBuffer : public ISavestate
+ {
+ public:
+ CSavestateFlatBuffer();
+ ~CSavestateFlatBuffer();
+
+ // Implementation of ISavestate
+ void Reset() override;
+ bool Serialize(const uint8_t *&data, size_t &size) const override;
+ SAVE_TYPE Type() const override;
+ uint8_t Slot() const override;
+ std::string Label() const override;
+ CDateTime Created() const override;
+ std::string GameFileName() const override;
+ uint64_t TimestampFrames() const override;
+ double TimestampWallClock() const override;
+ std::string GameClientID() const override;
+ std::string GameClientVersion() const override;
+ const uint8_t *GetMemoryData() const override;
+ size_t GetMemorySize() const override;
+ void SetType(SAVE_TYPE type) override;
+ void SetSlot(uint8_t slot) override;
+ void SetLabel(const std::string &label) override;
+ void SetCreated(const CDateTime &created) override;
+ void SetGameFileName(const std::string &gameFileName) override;
+ void SetTimestampFrames(uint64_t timestampFrames) override;
+ void SetTimestampWallClock(double timestampWallClock) override;
+ void SetGameClientID(const std::string &gameClient) override;
+ void SetGameClientVersion(const std::string &gameClient) override;
+ uint8_t *GetMemoryBuffer(size_t size) override;
+ void Finalize() override;
+ bool Deserialize(std::vector<uint8_t> data) override;
+
+ private:
+ /*!
+ * \brief Helper class to hold data needed in creation of a FlatBuffer
+ *
+ * The builder is used when deserializing from individual fields.
+ */
+ std::unique_ptr<flatbuffers::FlatBufferBuilder> m_builder;
+
+ /*!
+ * \brief System memory storage (for deserializing savestates)
+ *
+ * This memory is used when deserializing from a vector.
+ */
+ std::vector<uint8_t> m_data;
+
+ /*!
+ * \brief FlatBuffer struct used for accessing data
+ */
+ const Savestate *m_savestate = nullptr;
+
+ using StringOffset = flatbuffers::Offset<flatbuffers::String>;
+ using VectorOffset = flatbuffers::Offset<flatbuffers::Vector<uint8_t>>;
+
+ // Temporary deserialization variables
+ SAVE_TYPE m_type = SAVE_TYPE::UNKNOWN;
+ uint8_t m_slot = 0;
+ std::unique_ptr<StringOffset> m_labelOffset;
+ std::unique_ptr<StringOffset> m_createdOffset;
+ std::unique_ptr<StringOffset> m_gameFileNameOffset;
+ uint64_t m_timestampFrames = 0;
+ double m_timestampWallClock = 0.0;
+ std::unique_ptr<StringOffset> m_emulatorAddonIdOffset;
+ std::unique_ptr<StringOffset> m_emulatorVersionOffset;
+ std::unique_ptr<VectorOffset> m_memoryDataOffset;
+ };
+}
+}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateTranslator.h b/xbmc/cores/RetroPlayer/savestates/SavestateTypes.h
index acf35dc402..3fa2dd6147 100644
--- a/xbmc/cores/RetroPlayer/savestates/SavestateTranslator.h
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateTypes.h
@@ -8,19 +8,21 @@
#pragma once
-#include "Savestate.h"
-
-#include <string>
-
namespace KODI
{
namespace RETRO
{
- class CSavestateTranslator
+ /*!
+ * \brief Type of save action, either:
+ *
+ * - automatic (saving was not prompted by the user)
+ * - manual (user manually prompted the save)
+ */
+ enum class SAVE_TYPE
{
- public:
- static SAVETYPE TranslateType(const std::string& type);
- static std::string TranslateType(const SAVETYPE& type);
+ UNKNOWN,
+ AUTO,
+ MANUAL,
};
}
}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp
index 3fd8d8470a..39809c492d 100644
--- a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp
@@ -7,21 +7,14 @@
*/
#include "SavestateUtils.h"
-#include "Savestate.h"
#include "utils/URIUtils.h"
#define SAVESTATE_EXTENSION ".sav"
-#define METADATA_EXTENSION ".xml"
using namespace KODI;
using namespace RETRO;
-std::string CSavestateUtils::MakePath(const CSavestate& save)
+std::string CSavestateUtils::MakePath(const std::string &gamePath)
{
- return URIUtils::ReplaceExtension(save.GamePath(), SAVESTATE_EXTENSION);
-}
-
-std::string CSavestateUtils::MakeMetadataPath(const std::string &gamePath)
-{
- return URIUtils::ReplaceExtension(gamePath, METADATA_EXTENSION);
+ return URIUtils::ReplaceExtension(gamePath, SAVESTATE_EXTENSION);
}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h
index a252adf478..fb6e6b8b9d 100644
--- a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h
@@ -14,25 +14,16 @@ namespace KODI
{
namespace RETRO
{
- class CSavestate;
-
class CSavestateUtils
{
public:
/*!
- * \brief Calculate a path for the specified savestate
- *
- * The savestate path is the game path with the extension replaced by ".sav".
- */
- static std::string MakePath(const CSavestate& save);
-
- /*!
- * \brief Calculate a metadata path for the specified savestate
+ * \brief Calculate a savestate path for the specified game
*
- * The savestate metadata path is the game path with the extension replaced
- * by ".xml".
+ * The savestate path is the game path with the extension replaced
+ * by ".sav".
*/
- static std::string MakeMetadataPath(const std::string &gamePath);
+ static std::string MakePath(const std::string &gamePath);
};
}
}
diff --git a/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp
index 1b2cf3bf61..2f422493de 100644
--- a/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp
+++ b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp
@@ -9,9 +9,8 @@
#include "GUIDialogSelectGameClient.h"
#include "addons/AddonInstaller.h"
#include "addons/AddonManager.h"
-#include "cores/RetroPlayer/savestates/Savestate.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
-#include "cores/RetroPlayer/savestates/SavestateUtils.h"
#include "dialogs/GUIDialogSelect.h"
#include "filesystem/AddonsDirectory.h"
#include "games/addons/GameClient.h"
@@ -37,19 +36,19 @@ std::string CGUIDialogSelectGameClient::ShowAndGetGameClient(const std::string &
LogGameClients(candidates, installable);
std::string extension = URIUtils::GetExtension(gamePath);
- std::string xmlPath = RETRO::CSavestateUtils::MakeMetadataPath(gamePath);
// Load savestate
- RETRO::CSavestate save;
RETRO::CSavestateDatabase db;
- CLog::Log(LOGDEBUG, "Select game client dialog: Loading savestate metadata %s", CURL::GetRedacted(xmlPath).c_str());
- const bool bLoaded = db.GetSavestate(xmlPath, save);
+ std::unique_ptr<RETRO::ISavestate> save = db.CreateSavestate();
+
+ CLog::Log(LOGDEBUG, "Select game client dialog: Loading savestate metadata");
+ const bool bLoaded = db.GetSavestate(gamePath, *save);
// Get savestate game client
std::string saveGameClient;
if (bLoaded)
{
- saveGameClient = save.GameClient();
+ saveGameClient = save->GameClientID();
CLog::Log(LOGDEBUG, "Select game client dialog: Auto-selecting %s", saveGameClient.c_str());
}
diff --git a/xbmc/guilib/guiinfo/GamesGUIInfo.cpp b/xbmc/guilib/guiinfo/GamesGUIInfo.cpp
index c784c1d537..982d312a47 100644
--- a/xbmc/guilib/guiinfo/GamesGUIInfo.cpp
+++ b/xbmc/guilib/guiinfo/GamesGUIInfo.cpp
@@ -10,7 +10,6 @@
#include "FileItem.h"
#include "Util.h"
-#include "cores/RetroPlayer/savestates/SavestateDefines.h"
#include "cores/RetroPlayer/RetroPlayerUtils.h"
#include "games/tags/GameInfoTag.h"
#include "settings/MediaSettings.h"
@@ -25,6 +24,8 @@ using namespace KODI::GUILIB::GUIINFO;
using namespace KODI::GAME;
using namespace KODI::RETRO;
+#define FILEITEM_PROPERTY_SAVESTATE_DURATION "duration"
+
bool CGamesGUIInfo::InitCurrentItem(CFileItem *item)
{
if (item && item->IsGame())