Index: ps/trunk/source/ps/Mod.cpp =================================================================== --- ps/trunk/source/ps/Mod.cpp (revision 21335) +++ ps/trunk/source/ps/Mod.cpp (revision 21336) @@ -1,141 +1,142 @@ /* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "ps/Mod.h" #include #include "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" #include "lib/utf8.h" #include "ps/Filesystem.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" std::vector g_modsLoaded; CmdLineArgs g_args; JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedObject obj(cx, JS_NewPlainObject(cx)); const Paths paths(g_args); // loop over all possible paths OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; DirectoryNames modDirs; DirectoryNames modDirsUser; GetDirectoryEntries(modPath, NULL, &modDirs); // Sort modDirs so that we can do a fast lookup below std::sort(modDirs.begin(), modDirs.end()); PIVFS vfs = CreateVfs(); for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter) { vfs->Clear(); if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } GetDirectoryEntries(modUserPath, NULL, &modDirsUser); bool dev = InDevelopmentCopy(); for (DirectoryNames::iterator iter = modDirsUser.begin(); iter != modDirsUser.end(); ++iter) { // If we are in a dev copy we do not mount mods in the user mod folder that // are already present in the mod folder, thus we skip those here. if (dev && std::binary_search(modDirs.begin(), modDirs.end(), *iter)) continue; vfs->Clear(); if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } return JS::ObjectValue(*obj); } JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue availableMods(cx, GetAvailableMods(scriptInterface)); JS::RootedValue ret(cx, JS::ObjectValue(*JS_NewArrayObject(cx, 0))); // Index of the created array size_t j = 0; for (size_t i = 0; i < g_modsLoaded.size(); ++i) { // Ignore user and mod mod as they are irrelevant for compatibility checks if (g_modsLoaded[i] == "mod" || g_modsLoaded[i] == "user") continue; CStr version; JS::RootedValue modData(cx); if (scriptInterface.GetProperty(availableMods, g_modsLoaded[i].c_str(), &modData)) scriptInterface.GetProperty(modData, "version", version); scriptInterface.SetPropertyInt(ret, j++, std::vector{g_modsLoaded[i], version}); } return ret; } JS::Value Mod::GetEngineInfo(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue metainfo(cx); + JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(scriptInterface)); scriptInterface.Eval("({})", &metainfo); scriptInterface.SetProperty(metainfo, "engine_version", std::string(engine_version)); - scriptInterface.SetProperty(metainfo, "mods", JS::RootedValue(cx, Mod::GetLoadedModsWithVersions(scriptInterface))); + scriptInterface.SetProperty(metainfo, "mods", mods); scriptInterface.FreezeObject(metainfo, true); return metainfo; } Index: ps/trunk/source/ps/Replay.cpp =================================================================== --- ps/trunk/source/ps/Replay.cpp (revision 21335) +++ ps/trunk/source/ps/Replay.cpp (revision 21336) @@ -1,268 +1,269 @@ /* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "Replay.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/file/file_system.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "ps/Game.h" #include "ps/CLogger.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Pyrogenesis.h" #include "ps/Mod.h" #include "ps/Util.h" #include "ps/VisualReplay.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "simulation2/Simulation2.h" #include "simulation2/helpers/SimulationCommand.h" #include #include CReplayLogger::CReplayLogger(const ScriptInterface& scriptInterface) : m_ScriptInterface(scriptInterface), m_Stream(NULL) { } CReplayLogger::~CReplayLogger() { delete m_Stream; } void CReplayLogger::StartGame(JS::MutableHandleValue attribs) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); // Add timestamp, since the file-modification-date can change m_ScriptInterface.SetProperty(attribs, "timestamp", (double)std::time(nullptr)); // Add engine version and currently loaded mods for sanity checks when replaying m_ScriptInterface.SetProperty(attribs, "engine_version", CStr(engine_version)); - m_ScriptInterface.SetProperty(attribs, "mods", JS::RootedValue(cx, Mod::GetLoadedModsWithVersions(m_ScriptInterface))); + JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(m_ScriptInterface)); + m_ScriptInterface.SetProperty(attribs, "mods", mods); m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryName()); debug_printf("Writing replay to %s\n", m_Directory.string8().c_str()); m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc); *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n"; } void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector& commands) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); *m_Stream << "turn " << n << " " << turnLength << "\n"; for (SimulationCommand& command : commands) *m_Stream << "cmd " << command.player << " " << m_ScriptInterface.StringifyJSON(&command.data, false) << "\n"; *m_Stream << "end\n"; m_Stream->flush(); } void CReplayLogger::Hash(const std::string& hash, bool quick) { if (quick) *m_Stream << "hash-quick " << Hexify(hash) << "\n"; else *m_Stream << "hash " << Hexify(hash) << "\n"; } OsPath CReplayLogger::GetDirectory() const { return m_Directory; } //////////////////////////////////////////////////////////////// CReplayPlayer::CReplayPlayer() : m_Stream(NULL) { } CReplayPlayer::~CReplayPlayer() { delete m_Stream; } void CReplayPlayer::Load(const OsPath& path) { ENSURE(!m_Stream); m_Stream = new std::ifstream(OsString(path).c_str()); ENSURE(m_Stream->good()); } void CReplayPlayer::Replay(bool serializationtest, int rejointestturn, bool ooslog) { ENSURE(m_Stream); new CProfileViewer; new CProfileManager; g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); const int runtimeSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger); g_Game = new CGame(true, false); if (serializationtest) g_Game->GetSimulation2()->EnableSerializationTest(); if (rejointestturn > 0) g_Game->GetSimulation2()->EnableRejoinTest(rejointestturn); if (ooslog) g_Game->GetSimulation2()->EnableOOSLog(); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); // Initialise h_mgr so it doesn't crash when emitting sounds h_mgr_init(); std::vector commands; u32 turn = 0; u32 turnLength = 0; { JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); std::string type; while ((*m_Stream >> type).good()) { if (type == "start") { std::string line; std::getline(*m_Stream, line); JS::RootedValue attribs(cx); ENSURE(g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &attribs)); std::vector replayModList; g_Game->GetSimulation2()->GetScriptInterface().GetProperty(attribs, "mods", replayModList); for (const CStr& mod : replayModList) if (mod != "user" && std::find(g_modsLoaded.begin(), g_modsLoaded.end(), mod) == g_modsLoaded.end()) LOGWARNING("The mod '%s' is required by the replay file, but wasn't passed as an argument!", mod); for (const CStr& mod : g_modsLoaded) if (mod != "user" && std::find(replayModList.begin(), replayModList.end(), mod) == replayModList.end()) LOGWARNING("The mod '%s' wasn't used when creating this replay file, but was passed as an argument!", mod); g_Game->StartGame(&attribs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); } else if (type == "turn") { *m_Stream >> turn >> turnLength; debug_printf("Turn %u (%u)...\n", turn, turnLength); } else if (type == "cmd") { player_id_t player; *m_Stream >> player; std::string line; std::getline(*m_Stream, line); JS::RootedValue data(cx); g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &data); g_Game->GetSimulation2()->GetScriptInterface().FreezeObject(data, true); commands.emplace_back(SimulationCommand(player, cx, data)); } else if (type == "hash" || type == "hash-quick") { std::string replayHash; *m_Stream >> replayHash; bool quick = (type == "hash-quick"); if (turn % 100 == 0) { std::string hash; bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, quick); ENSURE(ok); std::string hexHash = Hexify(hash); if (hexHash == replayHash) debug_printf("hash ok (%s)\n", hexHash.c_str()); else debug_printf("HASH MISMATCH (%s != %s)\n", hexHash.c_str(), replayHash.c_str()); } } else if (type == "end") { { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); g_Game->GetSimulation2()->Update(turnLength, commands); commands.clear(); } g_Profiler.Frame(); if (turn % 20 == 0) g_ProfileViewer.SaveToFile(); } else { debug_printf("Unrecognised replay token %s\n", type.c_str()); } } } SAFE_DELETE(m_Stream); g_Profiler2.SaveToFile(); std::string hash; bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false); ENSURE(ok); debug_printf("# Final state: %s\n", Hexify(hash).c_str()); timer_DisplayClientTotals(); SAFE_DELETE(g_Game); // Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when // it's already destructed. g_ScriptRuntime.reset(); // Clean up delete &g_TexMan; delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); } Index: ps/trunk/source/ps/SavedGame.cpp =================================================================== --- ps/trunk/source/ps/SavedGame.cpp (revision 21335) +++ ps/trunk/source/ps/SavedGame.cpp (revision 21336) @@ -1,289 +1,290 @@ /* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "SavedGame.h" #include "graphics/GameView.h" #include "gui/GUIManager.h" #include "lib/allocators/shared_ptr.h" #include "lib/file/archive/archive_zip.h" #include "i18n/L10n.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Mod.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" // TODO: we ought to check version numbers when loading files Status SavedGames::SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone) { // Determine the filename to save under const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d"); const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave"); VfsPath filename; // Don't make this a static global like NextNumberedFilename expects, because // that wouldn't work when 'prefix' changes, and because it's not thread-safe size_t nextSaveNumber = 0; vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename); return Save(filename.Filename().string(), description, simulation, guiMetadataClone); } Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone) { JSContext* cx = simulation.GetScriptInterface().GetContext(); JSAutoRequest rq(cx); // Determine the filename to save under const VfsPath basenameFormat(L"saves/" + name); const VfsPath filename = basenameFormat.ChangeExtension(L".0adsave"); // ArchiveWriter_Zip can only write to OsPaths, not VfsPaths, // but we'd like to handle saved games via VFS. // To avoid potential confusion from writing with non-VFS then // reading the same file with VFS, we'll just write to a temporary // non-VFS path and then load and save again via VFS, // which is kind of a hack. OsPath tempSaveFileRealPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath)); tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave"; time_t now = time(NULL); // Construct the serialized state to be saved std::stringstream simStateStream; if (!simulation.SerializeState(simStateStream)) WARN_RETURN(ERR::FAIL); JS::RootedValue metadata(cx); JS::RootedValue initAttributes(cx, simulation.GetInitAttributes()); + JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface())); simulation.GetScriptInterface().Eval("({})", &metadata); simulation.GetScriptInterface().SetProperty(metadata, "engine_version", std::string(engine_version)); - simulation.GetScriptInterface().SetProperty(metadata, "mods", JS::RootedValue(cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface()))); + simulation.GetScriptInterface().SetProperty(metadata, "mods", mods); simulation.GetScriptInterface().SetProperty(metadata, "time", (double)now); simulation.GetScriptInterface().SetProperty(metadata, "playerID", g_Game->GetPlayerID()); simulation.GetScriptInterface().SetProperty(metadata, "initAttributes", initAttributes); JS::RootedValue guiMetadata(cx); simulation.GetScriptInterface().ReadStructuredClone(guiMetadataClone, &guiMetadata); // get some camera data JS::RootedValue cameraMetadata(cx); simulation.GetScriptInterface().Eval("({})", &cameraMetadata); simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosX", g_Game->GetView()->GetCameraPosX()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosY", g_Game->GetView()->GetCameraPosY()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosZ", g_Game->GetView()->GetCameraPosZ()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "RotX", g_Game->GetView()->GetCameraRotX()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "RotY", g_Game->GetView()->GetCameraRotY()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "Zoom", g_Game->GetView()->GetCameraZoom()); simulation.GetScriptInterface().SetProperty(guiMetadata, "camera", cameraMetadata); simulation.GetScriptInterface().SetProperty(metadata, "gui", guiMetadata); simulation.GetScriptInterface().SetProperty(metadata, "description", description); std::string metadataString = simulation.GetScriptInterface().StringifyJSON(&metadata, true); // Write the saved game as zip file containing the various components PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false); if (!archiveWriter) WARN_RETURN(ERR::FAIL); WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json")); WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat")); archiveWriter.reset(); // close the file WriteBuffer buffer; CFileInfo tempSaveFile; WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile)); buffer.Reserve(tempSaveFile.Size()); WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size())); WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size())); OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8()); debug_printf("Saved game to '%s'\n", realPath.string8().c_str()); return INFO::OK; } /** * Helper class for retrieving data from saved game archives */ class CGameLoader { NONCOPYABLE(CGameLoader); public: /** * @param scriptInterface the ScriptInterface used for loading metadata. * @param[out] savedState serialized simulation state stored as string of bytes, * loaded from simulation.dat inside the archive. * * Note: We use a different approach for returning the string and the metadata JS::Value. * We use a pointer for the string to avoid copies (efficiency). We don't use this approach * for the metadata because it would be error prone with rooting and the stack-based rooting * types and confusing (a chain of pointers pointing to other pointers). */ CGameLoader(const ScriptInterface& scriptInterface, std::string* savedState) : m_ScriptInterface(scriptInterface), m_Metadata(scriptInterface.GetJSRuntime()), m_SavedState(savedState) { } static void ReadEntryCallback(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData) { ((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile); } void ReadEntry(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); if (pathname == L"metadata.json") { std::string buffer; buffer.resize(fileInfo.Size()); WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size())); m_ScriptInterface.ParseJSON(buffer, &m_Metadata); } else if (pathname == L"simulation.dat" && m_SavedState) { m_SavedState->resize(fileInfo.Size()); WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size())); } } JS::Value GetMetadata() { return m_Metadata.get(); } private: const ScriptInterface& m_ScriptInterface; JS::PersistentRooted m_Metadata; std::string* m_SavedState; }; Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState) { // Determine the filename to load const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); // Don't crash just because file isn't found, this can happen if the file is deleted from the OS if (!VfsFileExists(filename)) return ERR::FILE_NOT_FOUND; OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) WARN_RETURN(ERR::FAIL); CGameLoader loader(scriptInterface, &savedState); WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader)); metadata.set(loader.GetMetadata()); return INFO::OK; } JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface) { TIMER(L"GetSavedGames"); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedObject games(cx, JS_NewArrayObject(cx, 0)); Status err; VfsPaths pathnames; err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames); WARN_IF_ERR(err); for (size_t i = 0; i < pathnames.size(); ++i) { OsPath realPath; err = g_VFS->GetRealPath(pathnames[i], realPath); if (err < 0) { DEBUG_WARN_ERR(err); continue; // skip this file } PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) { // Triggered by e.g. the file being open in another program LOGWARNING("Failed to read saved game '%s'", realPath.string8()); continue; // skip this file } CGameLoader loader(scriptInterface, NULL); err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader); if (err < 0) { DEBUG_WARN_ERR(err); continue; // skip this file } JS::RootedValue metadata(cx, loader.GetMetadata()); JS::RootedValue game(cx); scriptInterface.Eval("({})", &game); scriptInterface.SetProperty(game, "id", pathnames[i].Basename()); scriptInterface.SetProperty(game, "metadata", metadata); JS_SetElement(cx, games, i, game); } return JS::ObjectValue(*games); } bool SavedGames::DeleteSavedGame(const std::wstring& name) { const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); OsPath realpath; // Make sure it exists in VFS and find its real path if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK) return false; // Error // Remove from VFS if (g_VFS->RemoveFile(filename) != INFO::OK) return false; // Error // Delete actual file if (wunlink(realpath) != 0) return false; // Error // Successfully deleted file return true; }