LCOV - code coverage report
Current view: top level - source/ps - SavedGame.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 1 129 0.8 %
Date: 2023-01-19 00:18:29 Functions: 2 12 16.7 %

          Line data    Source code
       1             : /* Copyright (C) 2021 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "SavedGame.h"
      21             : 
      22             : #include "graphics/GameView.h"
      23             : #include "i18n/L10n.h"
      24             : #include "lib/allocators/shared_ptr.h"
      25             : #include "lib/file/archive/archive_zip.h"
      26             : #include "lib/file/io/io.h"
      27             : #include "lib/utf8.h"
      28             : #include "maths/Vector3D.h"
      29             : #include "ps/CLogger.h"
      30             : #include "ps/Filesystem.h"
      31             : #include "ps/Game.h"
      32             : #include "ps/Mod.h"
      33             : #include "ps/Pyrogenesis.h"
      34             : #include "scriptinterface/Object.h"
      35             : #include "scriptinterface/JSON.h"
      36             : #include "scriptinterface/StructuredClone.h"
      37             : #include "simulation2/Simulation2.h"
      38             : 
      39             : // TODO: we ought to check version numbers when loading files
      40             : 
      41           0 : Status SavedGames::SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const Script::StructuredClone& guiMetadataClone)
      42             : {
      43             :     // Determine the filename to save under
      44           0 :     const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d");
      45           0 :     const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave");
      46           0 :     VfsPath filename;
      47             : 
      48             :     // Don't make this a static global like NextNumberedFilename expects, because
      49             :     // that wouldn't work when 'prefix' changes, and because it's not thread-safe
      50           0 :     size_t nextSaveNumber = 0;
      51           0 :     vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename);
      52             : 
      53           0 :     return Save(filename.Filename().string(), description, simulation, guiMetadataClone);
      54             : }
      55             : 
      56           0 : Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, const Script::StructuredClone& guiMetadataClone)
      57             : {
      58           0 :     ScriptRequest rq(simulation.GetScriptInterface());
      59             : 
      60             :     // Determine the filename to save under
      61           0 :     const VfsPath basenameFormat(L"saves/" + name);
      62           0 :     const VfsPath filename = basenameFormat.ChangeExtension(L".0adsave");
      63             : 
      64             :     // ArchiveWriter_Zip can only write to OsPaths, not VfsPaths,
      65             :     // but we'd like to handle saved games via VFS.
      66             :     // To avoid potential confusion from writing with non-VFS then
      67             :     // reading the same file with VFS, we'll just write to a temporary
      68             :     // non-VFS path and then load and save again via VFS,
      69             :     // which is kind of a hack.
      70             : 
      71           0 :     OsPath tempSaveFileRealPath;
      72           0 :     WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath));
      73           0 :     tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave";
      74             : 
      75           0 :     time_t now = time(NULL);
      76             : 
      77             :     // Construct the serialized state to be saved
      78             : 
      79           0 :     std::stringstream simStateStream;
      80           0 :     if (!simulation.SerializeState(simStateStream))
      81           0 :         WARN_RETURN(ERR::FAIL);
      82             : 
      83           0 :     JS::RootedValue initAttributes(rq.cx, simulation.GetInitAttributes());
      84           0 :     JS::RootedValue mods(rq.cx);
      85           0 :     Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData());
      86             : 
      87           0 :     JS::RootedValue metadata(rq.cx);
      88             : 
      89           0 :     Script::CreateObject(
      90             :         rq,
      91             :         &metadata,
      92             :         "engine_version", engine_version,
      93           0 :         "time", static_cast<double>(now),
      94           0 :         "playerID", g_Game->GetPlayerID(),
      95             :         "mods", mods,
      96             :         "initAttributes", initAttributes);
      97             : 
      98           0 :     JS::RootedValue guiMetadata(rq.cx);
      99           0 :     Script::ReadStructuredClone(rq, guiMetadataClone, &guiMetadata);
     100             : 
     101             :     // get some camera data
     102           0 :     const CVector3D cameraPosition = g_Game->GetView()->GetCameraPosition();
     103           0 :     const CVector3D cameraRotation = g_Game->GetView()->GetCameraRotation();
     104             : 
     105           0 :     JS::RootedValue cameraMetadata(rq.cx);
     106             : 
     107           0 :     Script::CreateObject(
     108             :         rq,
     109             :         &cameraMetadata,
     110             :         "PosX", cameraPosition.X,
     111             :         "PosY", cameraPosition.Y,
     112             :         "PosZ", cameraPosition.Z,
     113             :         "RotX", cameraRotation.X,
     114             :         "RotY", cameraRotation.Y,
     115           0 :         "Zoom", g_Game->GetView()->GetCameraZoom());
     116             : 
     117           0 :     Script::SetProperty(rq, guiMetadata, "camera", cameraMetadata);
     118             : 
     119           0 :     Script::SetProperty(rq, metadata, "gui", guiMetadata);
     120           0 :     Script::SetProperty(rq, metadata, "description", description);
     121             : 
     122           0 :     std::string metadataString = Script::StringifyJSON(rq, &metadata, true);
     123             : 
     124             :     // Write the saved game as zip file containing the various components
     125           0 :     PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false);
     126           0 :     if (!archiveWriter)
     127           0 :         WARN_RETURN(ERR::FAIL);
     128             : 
     129           0 :     WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json"));
     130           0 :     WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat"));
     131           0 :     archiveWriter.reset(); // close the file
     132             : 
     133           0 :     WriteBuffer buffer;
     134           0 :     CFileInfo tempSaveFile;
     135           0 :     WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile));
     136           0 :     buffer.Reserve(tempSaveFile.Size());
     137           0 :     WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size()));
     138           0 :     WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size()));
     139             : 
     140           0 :     OsPath realPath;
     141           0 :     WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
     142           0 :     LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8());
     143           0 :     debug_printf("Saved game to '%s'\n", realPath.string8().c_str());
     144             : 
     145           0 :     return INFO::OK;
     146             : }
     147             : 
     148             : /**
     149             :  * Helper class for retrieving data from saved game archives
     150             :  */
     151           0 : class CGameLoader
     152             : {
     153             :     NONCOPYABLE(CGameLoader);
     154             : public:
     155             : 
     156             :     /**
     157             :      * @param scriptInterface the ScriptInterface used for loading metadata.
     158             :      * @param[out] savedState serialized simulation state stored as string of bytes,
     159             :      *  loaded from simulation.dat inside the archive.
     160             :      *
     161             :      * Note: We use a different approach for returning the string and the metadata JS::Value.
     162             :      * We use a pointer for the string to avoid copies (efficiency). We don't use this approach
     163             :      * for the metadata because it would be error prone with rooting and the stack-based rooting
     164             :      * types and confusing (a chain of pointers pointing to other pointers).
     165             :      */
     166           0 :     CGameLoader(const ScriptInterface& scriptInterface, std::string* savedState) :
     167             :         m_ScriptInterface(scriptInterface),
     168           0 :         m_SavedState(savedState)
     169             :     {
     170           0 :         ScriptRequest rq(scriptInterface);
     171           0 :         m_Metadata.init(rq.cx);
     172           0 :     }
     173             : 
     174           0 :     static void ReadEntryCallback(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData)
     175             :     {
     176           0 :         ((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile);
     177           0 :     }
     178             : 
     179           0 :     void ReadEntry(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile)
     180             :     {
     181           0 :         if (pathname == L"metadata.json")
     182             :         {
     183           0 :             std::string buffer;
     184           0 :             buffer.resize(fileInfo.Size());
     185           0 :             WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size()));
     186           0 :             Script::ParseJSON(ScriptRequest(m_ScriptInterface), buffer, &m_Metadata);
     187             :         }
     188           0 :         else if (pathname == L"simulation.dat" && m_SavedState)
     189             :         {
     190           0 :             m_SavedState->resize(fileInfo.Size());
     191           0 :             WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size()));
     192             :         }
     193           0 :     }
     194             : 
     195           0 :     JS::Value GetMetadata()
     196             :     {
     197           0 :         return m_Metadata.get();
     198             :     }
     199             : 
     200             : private:
     201             : 
     202             :     const ScriptInterface& m_ScriptInterface;
     203             :     JS::PersistentRooted<JS::Value> m_Metadata;
     204             :     std::string* m_SavedState;
     205             : };
     206             : 
     207           0 : Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState)
     208             : {
     209             :     // Determine the filename to load
     210           0 :     const VfsPath basename(L"saves/" + name);
     211           0 :     const VfsPath filename = basename.ChangeExtension(L".0adsave");
     212             : 
     213             :     // Don't crash just because file isn't found, this can happen if the file is deleted from the OS
     214           0 :     if (!VfsFileExists(filename))
     215           0 :         return ERR::FILE_NOT_FOUND;
     216             : 
     217           0 :     OsPath realPath;
     218           0 :     WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
     219             : 
     220           0 :     PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
     221           0 :     if (!archiveReader)
     222           0 :         WARN_RETURN(ERR::FAIL);
     223             : 
     224           0 :     CGameLoader loader(scriptInterface, &savedState);
     225           0 :     WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader));
     226           0 :     metadata.set(loader.GetMetadata());
     227             : 
     228           0 :     return INFO::OK;
     229             : }
     230             : 
     231           0 : JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface)
     232             : {
     233           0 :     TIMER(L"GetSavedGames");
     234           0 :     ScriptRequest rq(scriptInterface);
     235             : 
     236           0 :     JS::RootedValue games(rq.cx);
     237           0 :     Script::CreateArray(rq, &games);
     238             : 
     239             :     Status err;
     240             : 
     241           0 :     VfsPaths pathnames;
     242           0 :     err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames);
     243           0 :     WARN_IF_ERR(err);
     244             : 
     245           0 :     for (size_t i = 0; i < pathnames.size(); ++i)
     246             :     {
     247           0 :         OsPath realPath;
     248           0 :         err = g_VFS->GetRealPath(pathnames[i], realPath);
     249           0 :         if (err < 0)
     250             :         {
     251           0 :             DEBUG_WARN_ERR(err);
     252           0 :             continue; // skip this file
     253             :         }
     254             : 
     255           0 :         PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
     256           0 :         if (!archiveReader)
     257             :         {
     258             :             // Triggered by e.g. the file being open in another program
     259           0 :             LOGWARNING("Failed to read saved game '%s'", realPath.string8());
     260           0 :             continue; // skip this file
     261             :         }
     262             : 
     263           0 :         CGameLoader loader(scriptInterface, NULL);
     264           0 :         err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader);
     265           0 :         if (err < 0)
     266             :         {
     267           0 :             DEBUG_WARN_ERR(err);
     268           0 :             continue; // skip this file
     269             :         }
     270           0 :         JS::RootedValue metadata(rq.cx, loader.GetMetadata());
     271             : 
     272           0 :         JS::RootedValue game(rq.cx);
     273           0 :         Script::CreateObject(
     274             :             rq,
     275             :             &game,
     276           0 :             "id", pathnames[i].Basename(),
     277             :             "metadata", metadata);
     278             : 
     279           0 :         Script::SetPropertyInt(rq, games, i, game);
     280             :     }
     281             : 
     282           0 :     return games;
     283             : }
     284             : 
     285           0 : bool SavedGames::DeleteSavedGame(const std::wstring& name)
     286             : {
     287           0 :     const VfsPath basename(L"saves/" + name);
     288           0 :     const VfsPath filename = basename.ChangeExtension(L".0adsave");
     289           0 :     OsPath realpath;
     290             : 
     291             :     // Make sure it exists in VFS and find its path
     292           0 :     if (!VfsFileExists(filename) || g_VFS->GetOriginalPath(filename, realpath) != INFO::OK)
     293           0 :         return false; // Error
     294             : 
     295             :     // Remove from VFS
     296           0 :     if (g_VFS->RemoveFile(filename) != INFO::OK)
     297           0 :         return false; // Error
     298             : 
     299             :     // Delete actual file
     300           0 :     if (wunlink(realpath) != 0)
     301           0 :         return false; // Error
     302             : 
     303             :     // Successfully deleted file
     304           0 :     return true;
     305           3 : }

Generated by: LCOV version 1.13