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 : }
|