Index: ps/trunk/source/graphics/MapGenerator.cpp
===================================================================
--- ps/trunk/source/graphics/MapGenerator.cpp (revision 27943)
+++ ps/trunk/source/graphics/MapGenerator.cpp (revision 27944)
@@ -1,421 +1,428 @@
/* Copyright (C) 2023 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 "MapGenerator.h"
#include "graphics/MapIO.h"
#include "graphics/Patch.h"
#include "graphics/Terrain.h"
#include "lib/status.h"
#include "lib/timer.h"
#include "lib/file/vfs/vfs_path.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/FileIo.h"
#include "ps/Profile.h"
-#include "ps/TaskManager.h"
#include "ps/scripting/JSInterface_VFS.h"
+#include "ps/TemplateLoader.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/JSON.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptConversions.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/helpers/MapEdgeTiles.h"
+#include
+#include
#include
#include
-// TODO: Maybe this should be optimized depending on the map size.
-constexpr int RMS_CONTEXT_SIZE = 96 * 1024 * 1024;
-
extern bool IsQuitRequested();
-static bool
-MapGeneratorInterruptCallback(JSContext* UNUSED(cx))
+namespace
+{
+bool MapGenerationInterruptCallback(JSContext* UNUSED(cx))
{
// This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents
if (IsQuitRequested())
{
LOGWARNING("Quit requested!");
return false;
}
return true;
}
-CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) :
- m_ScriptInterface(scriptInterface)
-{}
-
-CMapGeneratorWorker::~CMapGeneratorWorker()
-{
- // Cancel or wait for the task to end.
- m_WorkerThread.CancelOrWait();
-}
-
-void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings)
+/**
+ * Provides callback's for the JavaScript.
+ */
+class CMapGenerationCallbacks
{
- std::lock_guard lock(m_WorkerMutex);
-
- // Set progress to positive value
- m_Progress.store(1);
- m_ScriptPath = scriptFile;
- m_Settings = settings;
-
- // Start generating the map asynchronously.
- m_WorkerThread = Threading::TaskManager::Instance().PushTask([this]() {
- PROFILE2("Map Generation");
+public:
+ // Only the constructor and the destructor are called by C++.
- std::shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE);
+ CMapGenerationCallbacks(std::atomic& progress, ScriptInterface& scriptInterface,
+ Script::StructuredClone& mapData, const u16 flags) :
+ m_Progress{progress},
+ m_ScriptInterface{scriptInterface},
+ m_MapData{mapData}
+ {
+ m_ScriptInterface.SetCallbackData(static_cast(this));
// Enable the script to be aborted
- JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback);
+ JS_AddInterruptCallback(m_ScriptInterface.GetGeneralJSContext(),
+ &MapGenerationInterruptCallback);
- m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext);
+ // Set initial seed, callback data.
+ // Expose functions, globals and classes relevant to the map scripts.
+#define REGISTER_MAPGEN_FUNC(func) \
+ ScriptFunction::Register<&CMapGenerationCallbacks::func, \
+ ScriptInterface::ObjectFromCBData>(rq, #func, flags);
- // Run map generation scripts
- if (!Run() || m_Progress.load() > 0)
- {
- // Don't leave progress in an unknown state, if generator failed, set it to -1
- m_Progress.store(-1);
- }
+ // VFS
+ JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(m_ScriptInterface, flags);
- SAFE_DELETE(m_ScriptInterface);
+ // Globalscripts may use VFS script functions
+ m_ScriptInterface.LoadGlobalScripts();
- // At this point the random map scripts are done running, so the thread has no further purpose
- // and can die. The data will be stored in m_MapData already if successful, or m_Progress
- // will contain an error value on failure.
- });
-}
+ // File loading
+ ScriptRequest rq(m_ScriptInterface);
+ REGISTER_MAPGEN_FUNC(LoadLibrary);
+ REGISTER_MAPGEN_FUNC(LoadHeightmapImage);
+ REGISTER_MAPGEN_FUNC(LoadMapTerrain);
+
+ // Template functions
+ REGISTER_MAPGEN_FUNC(GetTemplate);
+ REGISTER_MAPGEN_FUNC(TemplateExists);
+ REGISTER_MAPGEN_FUNC(FindTemplates);
+ REGISTER_MAPGEN_FUNC(FindActorTemplates);
+
+ // Progression and profiling
+ REGISTER_MAPGEN_FUNC(SetProgress);
+ REGISTER_MAPGEN_FUNC(GetMicroseconds);
+ REGISTER_MAPGEN_FUNC(ExportMap);
+
+ // Engine constants
+
+ // Length of one tile of the terrain grid in metres.
+ // Useful to transform footprint sizes to the tilegrid coordinate system.
+ m_ScriptInterface.SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE));
-bool CMapGeneratorWorker::Run()
-{
- ScriptRequest rq(m_ScriptInterface);
+ // Number of impassable tiles at the map border
+ m_ScriptInterface.SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES));
- // Parse settings
- JS::RootedValue settingsVal(rq.cx);
- if (!Script::ParseJSON(rq, m_Settings, &settingsVal) && settingsVal.isUndefined())
- {
- LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings");
- return false;
+#undef REGISTER_MAPGEN_FUNC
}
- // Prevent unintentional modifications to the settings object by random map scripts
- if (!Script::FreezeObject(rq, settingsVal, true))
+ ~CMapGenerationCallbacks()
{
- LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings");
- return false;
+ JS_AddInterruptCallback(m_ScriptInterface.GetGeneralJSContext(), nullptr);
+ m_ScriptInterface.SetCallbackData(nullptr);
}
- // Init RNG seed
- u32 seed = 0;
- if (!Script::HasProperty(rq, settingsVal, "Seed") ||
- !Script::GetProperty(rq, settingsVal, "Seed", seed))
- LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0");
-
- InitScriptInterface(seed);
+private:
- RegisterScriptFunctions_MapGenerator();
+ // These functions are called by JS.
- // Copy settings to global variable
- JS::RootedValue global(rq.cx, rq.globalValue());
- if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true))
+ /**
+ * Load all scripts of the given library
+ *
+ * @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/)
+ * @return true if all scripts ran successfully, false if there's an error
+ */
+ bool LoadLibrary(const VfsPath& libraryName)
{
- LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings");
- return false;
- }
+ // Ignore libraries that are already loaded
+ if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end())
+ return true;
- // Load RMS
- LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8());
- if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath))
- {
- LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8());
- return false;
- }
+ // Mark this as loaded, to prevent it recursively loading itself
+ m_LoadedLibraries.insert(libraryName);
- return true;
-}
+ VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath();
+ VfsPaths pathnames;
-#define REGISTER_MAPGEN_FUNC(func) \
- ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData>(rq, #func);
-#define REGISTER_MAPGEN_FUNC_NAME(func, name) \
- ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData>(rq, name);
-
-void CMapGeneratorWorker::InitScriptInterface(const u32 seed)
-{
- m_ScriptInterface->SetCallbackData(static_cast(this));
-
- m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG);
- m_MapGenRNG.seed(seed);
-
- // VFS
- JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(*m_ScriptInterface);
-
- // Globalscripts may use VFS script functions
- m_ScriptInterface->LoadGlobalScripts();
-
- // File loading
- ScriptRequest rq(m_ScriptInterface);
- REGISTER_MAPGEN_FUNC_NAME(LoadScripts, "LoadLibrary");
- REGISTER_MAPGEN_FUNC_NAME(LoadHeightmap, "LoadHeightmapImage");
- REGISTER_MAPGEN_FUNC(LoadMapTerrain);
-
- // Engine constants
-
- // Length of one tile of the terrain grid in metres.
- // Useful to transform footprint sizes to the tilegrid coordinate system.
- m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE));
-
- // Number of impassable tiles at the map border
- m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES));
-}
-
-void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator()
-{
- ScriptRequest rq(m_ScriptInterface);
-
- // Template functions
- REGISTER_MAPGEN_FUNC(GetTemplate);
- REGISTER_MAPGEN_FUNC(TemplateExists);
- REGISTER_MAPGEN_FUNC(FindTemplates);
- REGISTER_MAPGEN_FUNC(FindActorTemplates);
-
- // Progression and profiling
- REGISTER_MAPGEN_FUNC(SetProgress);
- REGISTER_MAPGEN_FUNC(GetMicroseconds);
- REGISTER_MAPGEN_FUNC(ExportMap);
-}
-
-#undef REGISTER_MAPGEN_FUNC
-#undef REGISTER_MAPGEN_FUNC_NAME
-
-int CMapGeneratorWorker::GetProgress() const
-{
- return m_Progress.load();
-}
+ // Load all scripts in mapgen directory
+ Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames);
+ if (ret == INFO::OK)
+ {
+ for (const VfsPath& p : pathnames)
+ {
+ LOGMESSAGE("Loading map generator script '%s'", p.string8());
-double CMapGeneratorWorker::GetMicroseconds()
-{
- return JS_Now();
-}
+ if (!m_ScriptInterface.LoadGlobalScriptFile(p))
+ {
+ LOGERROR("CMapGenerationCallbacks::LoadScripts: Failed to load script '%s'",
+ p.string8());
+ return false;
+ }
+ }
+ }
+ else
+ {
+ // Some error reading directory
+ wchar_t error[200];
+ LOGERROR(
+ "CMapGenerationCallbacks::LoadScripts: Error reading scripts in directory '%s': %s",
+ path.string8(),
+ utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error))));
+ return false;
+ }
-Script::StructuredClone CMapGeneratorWorker::GetResults()
-{
- std::lock_guard lock(m_WorkerMutex);
- return m_MapData;
-}
+ return true;
+ }
-void CMapGeneratorWorker::ExportMap(JS::HandleValue data)
-{
+ /**
+ * Finalize map generation and pass results from the script to the engine.
+ * The `data` has to be according to this format:
+ * https://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat
+ */
+ void ExportMap(JS::HandleValue data)
{
// Copy results
- std::lock_guard lock(m_WorkerMutex);
m_MapData = Script::WriteStructuredClone(ScriptRequest(m_ScriptInterface), data);
}
- m_Progress.store(0);
-}
-void CMapGeneratorWorker::SetProgress(int progress)
-{
- // When the task is started, `m_Progress` is only mutated by this thread.
- const int currentProgress = m_Progress.load();
- if (progress >= currentProgress)
- m_Progress.store(progress);
- else
- LOGWARNING("The random map script tried to reduce the loading progress from %d to %d",
- currentProgress, progress);
-}
-
-CParamNode CMapGeneratorWorker::GetTemplate(const std::string& templateName)
-{
- const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild();
- if (!templateRoot.IsOk())
- LOGERROR("Invalid template found for '%s'", templateName.c_str());
-
- return templateRoot;
-}
-
-bool CMapGeneratorWorker::TemplateExists(const std::string& templateName)
-{
- return m_TemplateLoader.TemplateExists(templateName);
-}
-
-std::vector CMapGeneratorWorker::FindTemplates(const std::string& path, bool includeSubdirectories)
-{
- return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES);
-}
+ /**
+ * Load an image file and return it as a height array.
+ */
+ JS::Value LoadHeightmapImage(const VfsPath& filename)
+ {
+ std::vector heightmap;
+ if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK)
+ {
+ LOGERROR("Could not load heightmap file '%s'", filename.string8());
+ return JS::UndefinedValue();
+ }
-std::vector CMapGeneratorWorker::FindActorTemplates(const std::string& path, bool includeSubdirectories)
-{
- return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES);
-}
+ ScriptRequest rq(m_ScriptInterface);
+ JS::RootedValue returnValue(rq.cx);
+ Script::ToJSVal(rq, &returnValue, heightmap);
+ return returnValue;
+ }
+
+ /**
+ * Load an Atlas terrain file (PMP) returning textures and heightmap.
+ *
+ * See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering
+ */
+ JS::Value LoadMapTerrain(const VfsPath& filename)
+ {
+ ScriptRequest rq(m_ScriptInterface);
-bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName)
-{
- // Ignore libraries that are already loaded
- if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end())
- return true;
+ if (!VfsFileExists(filename))
+ {
+ ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!",
+ filename.string8().c_str());
+ return JS::UndefinedValue();
+ }
- // Mark this as loaded, to prevent it recursively loading itself
- m_LoadedLibraries.insert(libraryName);
+ CFileUnpacker unpacker;
+ unpacker.Read(filename, "PSMP");
- VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath();
- VfsPaths pathnames;
+ if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION)
+ {
+ ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!",
+ filename.string8().c_str());
+ return JS::UndefinedValue();
+ }
- // Load all scripts in mapgen directory
- Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames);
- if (ret == INFO::OK)
- {
- for (const VfsPath& p : pathnames)
+ // unpack size
+ ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize();
+ size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1;
+
+ // unpack heightmap
+ std::vector heightmap;
+ heightmap.resize(SQR(verticesPerSide));
+ unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16));
+
+ // unpack texture names
+ size_t textureCount = unpacker.UnpackSize();
+ std::vector textureNames;
+ textureNames.reserve(textureCount);
+ for (size_t i = 0; i < textureCount; ++i)
{
- LOGMESSAGE("Loading map generator script '%s'", p.string8());
+ CStr texturename;
+ unpacker.UnpackString(texturename);
+ textureNames.push_back(texturename);
+ }
- if (!m_ScriptInterface->LoadGlobalScriptFile(p))
+ // unpack texture IDs per tile
+ ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE;
+ std::vector tiles;
+ tiles.resize(size_t(SQR(tilesPerSide)));
+ unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size());
+
+ // reorder by patches and store and save texture IDs per tile
+ std::vector textureIDs;
+ for (ssize_t x = 0; x < tilesPerSide; ++x)
+ {
+ size_t patchX = x / PATCH_SIZE;
+ size_t offX = x % PATCH_SIZE;
+ for (ssize_t y = 0; y < tilesPerSide; ++y)
{
- LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8());
- return false;
+ size_t patchY = y / PATCH_SIZE;
+ size_t offY = y % PATCH_SIZE;
+ // m_Priority and m_Tex2Index unused
+ textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) +
+ (offY * PATCH_SIZE + offX)].m_Tex1Index);
}
}
+
+ JS::RootedValue returnValue(rq.cx);
+
+ Script::CreateObject(
+ rq,
+ &returnValue,
+ "height", heightmap,
+ "textureNames", textureNames,
+ "textureIDs", textureIDs);
+
+ return returnValue;
}
- else
+
+ /**
+ * Sets the map generation progress, which is one of multiple stages
+ * determining the loading screen progress.
+ */
+ void SetProgress(int progress)
{
- // Some error reading directory
- wchar_t error[200];
- LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error))));
- return false;
+ // When the task is started, `m_Progress` is only mutated by this thread.
+ const int currentProgress = m_Progress.load();
+ if (progress >= currentProgress)
+ m_Progress.store(progress);
+ else
+ LOGWARNING("The random map script tried to reduce the loading progress from %d to %d",
+ currentProgress, progress);
}
- return true;
-}
-
-JS::Value CMapGeneratorWorker::LoadHeightmap(const VfsPath& filename)
-{
- std::vector heightmap;
- if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK)
+ /**
+ * Microseconds since the epoch.
+ */
+ double GetMicroseconds() const
{
- LOGERROR("Could not load heightmap file '%s'", filename.string8());
- return JS::UndefinedValue();
+ return JS_Now();
}
- ScriptRequest rq(m_ScriptInterface);
- JS::RootedValue returnValue(rq.cx);
- Script::ToJSVal(rq, &returnValue, heightmap);
- return returnValue;
-}
+ /**
+ * Return the template data of the given template name.
+ */
+ CParamNode GetTemplate(const std::string& templateName)
+ {
+ const CParamNode& templateRoot =
+ m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild();
+ if (!templateRoot.IsOk())
+ LOGERROR("Invalid template found for '%s'", templateName.c_str());
-// See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering
-JS::Value CMapGeneratorWorker::LoadMapTerrain(const VfsPath& filename)
-{
- ScriptRequest rq(m_ScriptInterface);
+ return templateRoot;
+ }
- if (!VfsFileExists(filename))
+ /**
+ * Check whether the given template exists.
+ */
+ bool TemplateExists(const std::string& templateName) const
{
- ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str());
- return JS::UndefinedValue();
+ return m_TemplateLoader.TemplateExists(templateName);
}
- CFileUnpacker unpacker;
- unpacker.Read(filename, "PSMP");
+ /**
+ * Returns all template names of simulation entity templates.
+ */
+ std::vector FindTemplates(const std::string& path, bool includeSubdirectories)
+ {
+ return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES);
+ }
- if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION)
+ /**
+ * Returns all template names of actors.
+ */
+ std::vector FindActorTemplates(const std::string& path, bool includeSubdirectories)
{
- ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", filename.string8().c_str());
- return JS::UndefinedValue();
+ return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES);
}
- // unpack size
- ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize();
- size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1;
+ /**
+ * Current map generation progress.
+ */
+ std::atomic& m_Progress;
- // unpack heightmap
- std::vector heightmap;
- heightmap.resize(SQR(verticesPerSide));
- unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16));
+ /**
+ * Provides the script context.
+ */
+ ScriptInterface& m_ScriptInterface;
- // unpack texture names
- size_t textureCount = unpacker.UnpackSize();
- std::vector textureNames;
- textureNames.reserve(textureCount);
- for (size_t i = 0; i < textureCount; ++i)
- {
- CStr texturename;
- unpacker.UnpackString(texturename);
- textureNames.push_back(texturename);
- }
+ /**
+ * Result of the mapscript generation including terrain, entities and environment settings.
+ */
+ Script::StructuredClone& m_MapData;
- // unpack texture IDs per tile
- ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE;
- std::vector tiles;
- tiles.resize(size_t(SQR(tilesPerSide)));
- unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size());
+ /**
+ * Currently loaded script librarynames.
+ */
+ std::set m_LoadedLibraries;
- // reorder by patches and store and save texture IDs per tile
- std::vector textureIDs;
- for (ssize_t x = 0; x < tilesPerSide; ++x)
- {
- size_t patchX = x / PATCH_SIZE;
- size_t offX = x % PATCH_SIZE;
- for (ssize_t y = 0; y < tilesPerSide; ++y)
- {
- size_t patchY = y / PATCH_SIZE;
- size_t offY = y % PATCH_SIZE;
- // m_Priority and m_Tex2Index unused
- textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index);
- }
- }
+ /**
+ * Backend to loading template data.
+ */
+ CTemplateLoader m_TemplateLoader;
+};
+} // anonymous namespace
- JS::RootedValue returnValue(rq.cx);
+Script::StructuredClone RunMapGenerationScript(std::atomic& progress, ScriptInterface& scriptInterface,
+ const VfsPath& script, const std::string& settings, const u16 flags)
+{
+ ScriptRequest rq(scriptInterface);
- Script::CreateObject(
- rq,
- &returnValue,
- "height", heightmap,
- "textureNames", textureNames,
- "textureIDs", textureIDs);
+ // Parse settings
+ JS::RootedValue settingsVal(rq.cx);
+ if (!Script::ParseJSON(rq, settings, &settingsVal) && settingsVal.isUndefined())
+ {
+ LOGERROR("RunMapGenerationScript: Failed to parse settings");
+ return nullptr;
+ }
- return returnValue;
-}
+ // Prevent unintentional modifications to the settings object by random map scripts
+ if (!Script::FreezeObject(rq, settingsVal, true))
+ {
+ LOGERROR("RunMapGenerationScript: Failed to deepfreeze settings");
+ return nullptr;
+ }
-//////////////////////////////////////////////////////////////////////////////////
-//////////////////////////////////////////////////////////////////////////////////
+ // Init RNG seed
+ u32 seed = 0;
+ if (!Script::HasProperty(rq, settingsVal, "Seed") ||
+ !Script::GetProperty(rq, settingsVal, "Seed", seed))
+ LOGWARNING("RunMapGenerationScript: No seed value specified - using 0");
-CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr))
-{
-}
+ boost::rand48 mapGenRNG{seed};
+ scriptInterface.ReplaceNondeterministicRNG(mapGenRNG);
-CMapGenerator::~CMapGenerator()
-{
- delete m_Worker;
-}
+ Script::StructuredClone mapData;
+ CMapGenerationCallbacks callbackData{progress, scriptInterface, mapData, flags};
-void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings)
-{
- m_Worker->Initialize(scriptFile, settings);
-}
+ // Copy settings to global variable
+ JS::RootedValue global(rq.cx, rq.globalValue());
+ if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, flags & JSPROP_READONLY,
+ flags & JSPROP_ENUMERATE))
+ {
+ LOGERROR("RunMapGenerationScript: Failed to define g_MapSettings");
+ return nullptr;
+ }
-int CMapGenerator::GetProgress() const
-{
- return m_Worker->GetProgress();
-}
+ // Load RMS
+ LOGMESSAGE("Loading RMS '%s'", script.string8());
+ if (!scriptInterface.LoadGlobalScriptFile(script))
+ {
+ LOGERROR("RunMapGenerationScript: Failed to load RMS '%s'", script.string8());
+ return nullptr;
+ }
-Script::StructuredClone CMapGenerator::GetResults()
-{
- return m_Worker->GetResults();
+ return mapData;
}
Index: ps/trunk/source/graphics/MapGenerator.h
===================================================================
--- ps/trunk/source/graphics/MapGenerator.h (revision 27943)
+++ ps/trunk/source/graphics/MapGenerator.h (revision 27944)
@@ -1,240 +1,44 @@
/* Copyright (C) 2023 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 .
*/
#ifndef INCLUDED_MAPGENERATOR
#define INCLUDED_MAPGENERATOR
-#include "ps/FileIo.h"
-#include "ps/Future.h"
-#include "ps/TemplateLoader.h"
+#include "lib/file/vfs/vfs_path.h"
#include "scriptinterface/StructuredClone.h"
#include
-#include
-#include
-#include
#include
-class CMapGeneratorWorker;
-
-/**
- * Random map generator interface. Initialized by CMapReader and then checked
- * periodically during loading, until it's finished (progress value is 0).
- *
- * The actual work is performed by CMapGeneratorWorker in a separate thread.
- */
-class CMapGenerator
-{
- NONCOPYABLE(CMapGenerator);
-
-public:
- CMapGenerator();
- ~CMapGenerator();
-
- /**
- * Start the map generator thread
- *
- * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js"
- * @param settings JSON string containing settings for the map generator
- */
- void GenerateMap(const VfsPath& scriptFile, const std::string& settings);
-
- /**
- * Get status of the map generator thread
- *
- * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error
- */
- int GetProgress() const;
-
- /**
- * Get random map data, according to this format:
- * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat
- *
- * @return StructuredClone containing map data
- */
- Script::StructuredClone GetResults();
-
-private:
- CMapGeneratorWorker* m_Worker;
-
-};
-
/**
- * Random map generator worker thread.
- * (This is run in a thread so that the GUI remains responsive while loading)
+ * Generate the map. This does take a long time.
*
- * Thread-safety:
- * - Initialize and constructor/destructor must be called from the main thread.
- * - ScriptInterface created and destroyed by thread
- * - StructuredClone used to return JS map data - JS:Values can't be used across threads/contexts.
+ * @param progress Destination to write the function progress to. You must not
+ * write to it while `RunMapGenerationScript` is running.
+ * @param script The VFS path for the script, e.g. "maps/random/latium.js".
+ * @param settings JSON string containing settings for the map generator.
+ * @param flags With thous flags the engine functions get registered
+ * `g_MapSettings` also respects this flags.
+ * @return If there is an error `nullptr` is returned. Otherwise random map
+ * data, according to this format:
+ * https://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat
*/
-class CMapGeneratorWorker
-{
-public:
- CMapGeneratorWorker(ScriptInterface* scriptInterface);
- ~CMapGeneratorWorker();
-
- /**
- * Start the map generator thread
- *
- * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js"
- * @param settings JSON string containing settings for the map generator
- */
- void Initialize(const VfsPath& scriptFile, const std::string& settings);
-
- /**
- * Get status of the map generator thread
- *
- * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error
- */
- int GetProgress() const;
-
- /**
- * Get random map data, according to this format:
- * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat
- *
- * @return StructuredClone containing map data
- */
- Script::StructuredClone GetResults();
-
- /**
- * Set initial seed, callback data.
- * Expose functions, globals and classes defined in this class relevant to the map and test scripts.
- */
- void InitScriptInterface(const u32 seed);
-
-private:
-
- /**
- * Expose functions defined in this class that are relevant to mapscripts but not the tests.
- */
- void RegisterScriptFunctions_MapGenerator();
-
- /**
- * Load all scripts of the given library
- *
- * @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/)
- * @return true if all scripts ran successfully, false if there's an error
- */
- bool LoadScripts(const VfsPath& libraryName);
-
- /**
- * Finalize map generation and pass results from the script to the engine.
- */
- void ExportMap(JS::HandleValue data);
-
- /**
- * Load an image file and return it as a height array.
- */
- JS::Value LoadHeightmap(const VfsPath& src);
-
- /**
- * Load an Atlas terrain file (PMP) returning textures and heightmap.
- */
- JS::Value LoadMapTerrain(const VfsPath& filename);
-
- /**
- * Sets the map generation progress, which is one of multiple stages determining the loading screen progress.
- */
- void SetProgress(int progress);
-
- /**
- * Microseconds since the epoch.
- */
- double GetMicroseconds();
-
- /**
- * Return the template data of the given template name.
- */
- CParamNode GetTemplate(const std::string& templateName);
-
- /**
- * Check whether the given template exists.
- */
- bool TemplateExists(const std::string& templateName);
-
- /**
- * Returns all template names of simulation entity templates.
- */
- std::vector FindTemplates(const std::string& path, bool includeSubdirectories);
-
- /**
- * Returns all template names of actors.
- */
- std::vector FindActorTemplates(const std::string& path, bool includeSubdirectories);
-
- /**
- * Perform the map generation.
- */
- bool Run();
-
- /**
- * Currently loaded script librarynames.
- */
- std::set m_LoadedLibraries;
-
- /**
- * Result of the mapscript generation including terrain, entities and environment settings.
- */
- Script::StructuredClone m_MapData;
-
- /**
- * Deterministic random number generator.
- */
- boost::rand48 m_MapGenRNG;
-
- /**
- * Current map generation progress.
- * Initialize to `-1`. If something happens before we start, that's a
- * failure.
- */
- std::atomic m_Progress{-1};
-
- /**
- * Provides the script context.
- */
- ScriptInterface* m_ScriptInterface;
-
- /**
- * Map generation script to run.
- */
- VfsPath m_ScriptPath;
-
- /**
- * Map and simulation settings chosen in the gamesetup stage.
- */
- std::string m_Settings;
-
- /**
- * Backend to loading template data.
- */
- CTemplateLoader m_TemplateLoader;
-
- /**
- * Holds the completion result of the asynchronous map generation.
- * TODO: this whole class could really be a future on its own.
- */
- Future m_WorkerThread;
-
- /**
- * Avoids thread synchronization issues.
- */
- std::mutex m_WorkerMutex;
-};
-
+Script::StructuredClone RunMapGenerationScript(std::atomic& progress,
+ ScriptInterface& scriptInterface, const VfsPath& script, const std::string& settings,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
#endif //INCLUDED_MAPGENERATOR
Index: ps/trunk/source/graphics/MapReader.cpp
===================================================================
--- ps/trunk/source/graphics/MapReader.cpp (revision 27943)
+++ ps/trunk/source/graphics/MapReader.cpp (revision 27944)
@@ -1,1656 +1,1675 @@
/* Copyright (C) 2023 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 "MapReader.h"
#include "graphics/Camera.h"
#include "graphics/CinemaManager.h"
#include "graphics/Entity.h"
#include "graphics/GameView.h"
#include "graphics/MapGenerator.h"
#include "graphics/Patch.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Loader.h"
+#include "ps/TaskManager.h"
#include "ps/World.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/PostprocManager.h"
#include "renderer/SkyManager.h"
#include "renderer/WaterManager.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
+#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/JSON.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpCinemaManager.h"
#include "simulation2/components/ICmpGarrisonHolder.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpTurretHolder.h"
#include "simulation2/components/ICmpVisual.h"
#include "simulation2/components/ICmpWaterManager.h"
#include
#if defined(_MSC_VER) && _MSC_VER > 1900
#pragma warning(disable: 4456) // Declaration hides previous local declaration.
#pragma warning(disable: 4458) // Declaration hides class member.
#endif
-CMapReader::CMapReader()
- : xml_reader(0), m_PatchesPerSide(0), m_MapGen(0)
-{
- cur_terrain_tex = 0; // important - resets generator state
-}
+// TODO: Maybe this should be optimized depending on the map size.
+constexpr int MAP_GENERATION_CONTEXT_SIZE{96 * MiB};
+
+CMapReader::CMapReader() = default;
// LoadMap: try to load the map from given file; reinitialise the scene to new data if successful
void CMapReader::LoadMap(const VfsPath& pathname, const ScriptContext& cx, JS::HandleValue settings, CTerrain *pTerrain_,
WaterManager* pWaterMan_, SkyManager* pSkyMan_,
CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_,
CSimulation2 *pSimulation2_, const CSimContext* pSimContext_, int playerID_, bool skipEntities)
{
pTerrain = pTerrain_;
pLightEnv = pLightEnv_;
pGameView = pGameView_;
pWaterMan = pWaterMan_;
pSkyMan = pSkyMan_;
pCinema = pCinema_;
pTrigMan = pTrigMan_;
pPostproc = pPostproc_;
pSimulation2 = pSimulation2_;
pSimContext = pSimContext_;
m_PlayerID = playerID_;
m_SkipEntities = skipEntities;
m_StartingCameraTarget = INVALID_ENTITY;
m_ScriptSettings.init(cx.GetGeneralJSContext(), settings);
filename_xml = pathname.ChangeExtension(L".xml");
// In some cases (particularly tests) we don't want to bother storing a large
// mostly-empty .pmp file, so we let the XML file specify basic terrain instead.
// If there's an .xml file and no .pmp, then we're probably in this XML-only mode
only_xml = false;
if (!VfsFileExists(pathname) && VfsFileExists(filename_xml))
{
only_xml = true;
}
file_format_version = CMapIO::FILE_VERSION; // default if there's no .pmp
if (!only_xml)
{
// [25ms]
unpacker.Read(pathname, "PSMP");
file_format_version = unpacker.GetVersion();
}
// check oldest supported version
if (file_format_version < FILE_READ_VERSION)
throw PSERROR_Game_World_MapLoadFailed("Could not load terrain file - too old version!");
// delete all existing entities
if (pSimulation2)
pSimulation2->ResetState();
// reset post effects
if (pPostproc)
pPostproc->SetPostEffect(L"default");
// load map or script settings script
if (settings.isUndefined())
LDR_Register([this](const double)
{
return LoadScriptSettings();
}, L"CMapReader::LoadScriptSettings", 50);
else
LDR_Register([this](const double)
{
return LoadRMSettings();
}, L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
LDR_Register([this](const double)
{
return LoadPlayerSettings();
}, L"CMapReader::LoadPlayerSettings", 50);
// unpack the data
if (!only_xml)
LDR_Register([this](const double)
{
return UnpackTerrain();
}, L"CMapReader::UnpackMap", 1200);
// read the corresponding XML file
LDR_Register([this](const double)
{
return ReadXML();
}, L"CMapReader::ReadXML", 50);
// apply terrain data to the world
LDR_Register([this](const double)
{
return ApplyTerrainData();
}, L"CMapReader::ApplyTerrainData", 5);
// read entities
LDR_Register([this](const double)
{
return ReadXMLEntities();
}, L"CMapReader::ReadXMLEntities", 5800);
// apply misc data to the world
LDR_Register([this](const double)
{
return ApplyData();
}, L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
LDR_Register([this](const double)
{
return LoadMapSettings();
}, L"CMapReader::LoadMapSettings", 5);
}
// LoadRandomMap: try to load the map data; reinitialise the scene to new data if successful
void CMapReader::LoadRandomMap(const CStrW& scriptFile, const ScriptContext& cx, JS::HandleValue settings, CTerrain *pTerrain_,
WaterManager* pWaterMan_, SkyManager* pSkyMan_,
CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_,
CSimulation2 *pSimulation2_, int playerID_)
{
pSimulation2 = pSimulation2_;
pSimContext = pSimulation2 ? &pSimulation2->GetSimContext() : NULL;
m_ScriptSettings.init(cx.GetGeneralJSContext(), settings);
pTerrain = pTerrain_;
pLightEnv = pLightEnv_;
pGameView = pGameView_;
pWaterMan = pWaterMan_;
pSkyMan = pSkyMan_;
pCinema = pCinema_;
pTrigMan = pTrigMan_;
pPostproc = pPostproc_;
m_PlayerID = playerID_;
m_SkipEntities = false;
m_StartingCameraTarget = INVALID_ENTITY;
// delete all existing entities
if (pSimulation2)
pSimulation2->ResetState();
only_xml = false;
// copy random map settings (before entity creation)
LDR_Register([this](const double)
{
return LoadRMSettings();
}, L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
LDR_Register([this](const double)
{
return LoadPlayerSettings();
}, L"CMapReader::LoadPlayerSettings", 50);
// load map generator with random map script
LDR_Register([this, scriptFile](const double)
{
- return GenerateMap(scriptFile);
- }, L"CMapReader::GenerateMap", 20000);
+ return StartMapGeneration(scriptFile);
+ }, L"CMapReader::StartMapGeneration", 1);
+
+ LDR_Register([this](const double)
+ {
+ return PollMapGeneration();
+ }, L"CMapReader::PollMapGeneration", 19999);
// parse RMS results into terrain structure
LDR_Register([this](const double)
{
return ParseTerrain();
}, L"CMapReader::ParseTerrain", 500);
// parse RMS results into environment settings
LDR_Register([this](const double)
{
return ParseEnvironment();
}, L"CMapReader::ParseEnvironment", 5);
// parse RMS results into camera settings
LDR_Register([this](const double)
{
return ParseCamera();
}, L"CMapReader::ParseCamera", 5);
// apply terrain data to the world
LDR_Register([this](const double)
{
return ApplyTerrainData();
}, L"CMapReader::ApplyTerrainData", 5);
// parse RMS results into entities
LDR_Register([this](const double)
{
return ParseEntities();
}, L"CMapReader::ParseEntities", 1000);
// apply misc data to the world
LDR_Register([this](const double)
{
return ApplyData();
}, L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
LDR_Register([this](const double)
{
return LoadMapSettings();
}, L"CMapReader::LoadMapSettings", 5);
}
// UnpackTerrain: unpack the terrain from the end of the input data stream
// - data: map size, heightmap, list of textures used by map, texture tile assignments
int CMapReader::UnpackTerrain()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
// first call to generator (this is skipped after first call,
// i.e. when the loop below was interrupted)
if (cur_terrain_tex == 0)
{
m_PatchesPerSide = (ssize_t)unpacker.UnpackSize();
// unpack heightmap [600us]
size_t verticesPerSide = m_PatchesPerSide*PATCH_SIZE+1;
m_Heightmap.resize(SQR(verticesPerSide));
unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16));
// unpack # textures
num_terrain_tex = unpacker.UnpackSize();
m_TerrainTextures.reserve(num_terrain_tex);
}
// unpack texture names; find handle for each texture.
// interruptible.
while (cur_terrain_tex < num_terrain_tex)
{
CStr texturename;
unpacker.UnpackString(texturename);
if (CTerrainTextureManager::IsInitialised())
{
CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename);
m_TerrainTextures.push_back(texentry);
}
cur_terrain_tex++;
LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex);
}
// unpack tile data [3ms]
ssize_t tilesPerSide = m_PatchesPerSide*PATCH_SIZE;
m_Tiles.resize(size_t(SQR(tilesPerSide)));
unpacker.UnpackRaw(&m_Tiles[0], sizeof(STileDesc)*m_Tiles.size());
// reset generator state.
cur_terrain_tex = 0;
return 0;
}
int CMapReader::ApplyTerrainData()
{
if (m_PatchesPerSide == 0)
{
// we'll probably crash when trying to use this map later
throw PSERROR_Game_World_MapLoadFailed("Error loading map: no terrain data.\nCheck application log for details.");
}
if (!only_xml)
{
// initialise the terrain
pTerrain->Initialize(m_PatchesPerSide, &m_Heightmap[0]);
if (CTerrainTextureManager::IsInitialised())
{
// setup the textures on the minipatches
STileDesc* tileptr = &m_Tiles[0];
for (ssize_t j=0; jGetPatch(i,j)->m_MiniPatches[m][k]; // can't fail
mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index];
mp.Priority = tileptr->m_Priority;
tileptr++;
}
}
}
}
}
}
CmpPtr cmpTerrain(*pSimContext, SYSTEM_ENTITY);
if (cmpTerrain)
cmpTerrain->ReloadTerrain();
return 0;
}
// ApplyData: take all the input data, and rebuild the scene from it
int CMapReader::ApplyData()
{
// copy over the lighting parameters
if (pLightEnv)
*pLightEnv = m_LightEnv;
CmpPtr cmpPlayerManager(*pSimContext, SYSTEM_ENTITY);
if (pGameView && cmpPlayerManager)
{
// Default to global camera (with constraints)
pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus());
// TODO: Starting rotation?
CmpPtr cmpPlayer(*pSimContext, cmpPlayerManager->GetPlayerByID(m_PlayerID));
if (cmpPlayer && cmpPlayer->HasStartingCamera())
{
// Use player starting camera
CFixedVector3D pos = cmpPlayer->GetStartingCameraPos();
pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
}
else if (m_StartingCameraTarget != INVALID_ENTITY)
{
// Point camera at entity
CmpPtr cmpPosition(*pSimContext, m_StartingCameraTarget);
if (cmpPosition)
{
CFixedVector3D pos = cmpPosition->GetPosition();
pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
}
}
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
PSRETURN CMapSummaryReader::LoadMap(const VfsPath& pathname)
{
VfsPath filename_xml = pathname.ChangeExtension(L".xml");
CXeromyces xmb_file;
if (xmb_file.Load(g_VFS, filename_xml, "scenario") != PSRETURN_OK)
return PSRETURN_File_ReadFailed;
// Define all the relevant elements used in the XML file
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(scenario);
EL(scriptsettings);
#undef AT
#undef EL
XMBElement root = xmb_file.GetRoot();
ENSURE(root.GetNodeName() == el_scenario);
XERO_ITER_EL(root, child)
{
int child_name = child.GetNodeName();
if (child_name == el_scriptsettings)
{
m_ScriptSettings = child.GetText();
}
}
return PSRETURN_OK;
}
void CMapSummaryReader::GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
ScriptRequest rq(scriptInterface);
Script::CreateObject(rq, ret);
if (m_ScriptSettings.empty())
return;
JS::RootedValue scriptSettingsVal(rq.cx);
Script::ParseJSON(rq, m_ScriptSettings, &scriptSettingsVal);
Script::SetProperty(rq, ret, "settings", scriptSettingsVal, false);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Holds various state data while reading maps, so that loading can be
// interrupted (e.g. to update the progress display) then later resumed.
class CXMLReader
{
NONCOPYABLE(CXMLReader);
public:
CXMLReader(const VfsPath& xml_filename, CMapReader& mapReader)
: m_MapReader(mapReader), nodes(NULL, 0, NULL)
{
Init(xml_filename);
}
CStr ReadScriptSettings();
// read everything except for entities
void ReadXML();
// return semantics: see Loader.cpp!LoadFunc.
int ProgressiveReadEntities();
private:
CXeromyces xmb_file;
CMapReader& m_MapReader;
int el_entity;
int el_tracks;
int el_template, el_player;
int el_position, el_orientation, el_obstruction;
int el_garrison;
int el_turrets;
int el_actor;
int at_x;
int at_y;
int at_z;
int at_group, at_group2;
int at_angle;
int at_uid;
int at_seed;
int at_turret;
XMBElementList nodes; // children of root
// loop counters
size_t node_idx;
size_t entity_idx;
// # entities+nonentities processed and total (for progress calc)
int completed_jobs, total_jobs;
// maximum used entity ID, so we can safely allocate new ones
entity_id_t max_uid;
void Init(const VfsPath& xml_filename);
void ReadTerrain(XMBElement parent);
void ReadEnvironment(XMBElement parent);
void ReadCamera(XMBElement parent);
void ReadPaths(XMBElement parent);
void ReadTriggers(XMBElement parent);
int ReadEntities(XMBElement parent, double end_time);
};
void CXMLReader::Init(const VfsPath& xml_filename)
{
// must only assign once, so do it here
node_idx = entity_idx = 0;
if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK)
throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!");
// define the elements and attributes that are frequently used in the XML file,
// so we don't need to do lots of string construction and comparison when
// reading the data.
// (Needs to be synchronised with the list in CXMLReader - ugh)
#define EL(x) el_##x = xmb_file.GetElementID(#x)
#define AT(x) at_##x = xmb_file.GetAttributeID(#x)
EL(entity);
EL(tracks);
EL(template);
EL(player);
EL(position);
EL(garrison);
EL(turrets);
EL(orientation);
EL(obstruction);
EL(actor);
AT(x); AT(y); AT(z);
AT(group); AT(group2);
AT(angle);
AT(uid);
AT(seed);
AT(turret);
#undef AT
#undef EL
XMBElement root = xmb_file.GetRoot();
ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
nodes = root.GetChildNodes();
// find out total number of entities+nonentities
// (used when calculating progress)
completed_jobs = 0;
total_jobs = 0;
for (XMBElement node : nodes)
total_jobs += node.GetChildNodes().size();
// Find the maximum entity ID, so we can safely allocate new IDs without conflicts
max_uid = SYSTEM_ENTITY;
XMBElement ents = nodes.GetFirstNamedItem(xmb_file.GetElementID("Entities"));
XERO_ITER_EL(ents, ent)
{
CStr uid = ent.GetAttributes().GetNamedItem(at_uid);
max_uid = std::max(max_uid, (entity_id_t)uid.ToUInt());
}
}
CStr CXMLReader::ReadScriptSettings()
{
XMBElement root = xmb_file.GetRoot();
ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
nodes = root.GetChildNodes();
XMBElement settings = nodes.GetFirstNamedItem(xmb_file.GetElementID("ScriptSettings"));
return settings.GetText();
}
void CXMLReader::ReadTerrain(XMBElement parent)
{
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
AT(patches);
AT(texture);
AT(priority);
AT(height);
#undef AT
ssize_t patches = 9;
CStr texture = "grass1_spring";
int priority = 0;
u16 height = 16384;
XERO_ITER_ATTR(parent, attr)
{
if (attr.Name == at_patches)
patches = attr.Value.ToInt();
else if (attr.Name == at_texture)
texture = attr.Value;
else if (attr.Name == at_priority)
priority = attr.Value.ToInt();
else if (attr.Name == at_height)
height = (u16)attr.Value.ToInt();
}
m_MapReader.m_PatchesPerSide = patches;
// Load the texture
CTerrainTextureEntry* texentry = nullptr;
if (CTerrainTextureManager::IsInitialised())
texentry = g_TexMan.FindTexture(texture);
m_MapReader.pTerrain->Initialize(patches, NULL);
// Fill the heightmap
u16* heightmap = m_MapReader.pTerrain->GetHeightMap();
ssize_t verticesPerSide = m_MapReader.pTerrain->GetVerticesPerSide();
for (ssize_t i = 0; i < SQR(verticesPerSide); ++i)
heightmap[i] = height;
// Fill the texture map
for (ssize_t pz = 0; pz < patches; ++pz)
{
for (ssize_t px = 0; px < patches; ++px)
{
CPatch* patch = m_MapReader.pTerrain->GetPatch(px, pz); // can't fail
for (ssize_t z = 0; z < PATCH_SIZE; ++z)
{
for (ssize_t x = 0; x < PATCH_SIZE; ++x)
{
patch->m_MiniPatches[z][x].Tex = texentry;
patch->m_MiniPatches[z][x].Priority = priority;
}
}
}
}
}
void CXMLReader::ReadEnvironment(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(posteffect);
EL(skyset);
EL(suncolor);
EL(sunelevation);
EL(sunrotation);
EL(ambientcolor);
EL(water);
EL(waterbody);
EL(type);
EL(color);
EL(tint);
EL(height);
EL(waviness);
EL(murkiness);
EL(windangle);
EL(fog);
EL(fogcolor);
EL(fogfactor);
EL(fogthickness);
EL(postproc);
EL(brightness);
EL(contrast);
EL(saturation);
EL(bloom);
AT(r); AT(g); AT(b);
#undef AT
#undef EL
XERO_ITER_EL(parent, element)
{
int element_name = element.GetNodeName();
XMBAttributeList attrs = element.GetAttributes();
if (element_name == el_skyset)
{
if (m_MapReader.pSkyMan)
m_MapReader.pSkyMan->SetSkySet(element.GetText().FromUTF8());
}
else if (element_name == el_suncolor)
{
m_MapReader.m_LightEnv.m_SunColor = RGBColor(
attrs.GetNamedItem(at_r).ToFloat(),
attrs.GetNamedItem(at_g).ToFloat(),
attrs.GetNamedItem(at_b).ToFloat());
}
else if (element_name == el_sunelevation)
{
m_MapReader.m_LightEnv.m_Elevation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_sunrotation)
{
m_MapReader.m_LightEnv.m_Rotation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_ambientcolor)
{
m_MapReader.m_LightEnv.m_AmbientColor = RGBColor(
attrs.GetNamedItem(at_r).ToFloat(),
attrs.GetNamedItem(at_g).ToFloat(),
attrs.GetNamedItem(at_b).ToFloat());
}
else if (element_name == el_fog)
{
XERO_ITER_EL(element, fog)
{
int fog_element_name = fog.GetNodeName();
if (fog_element_name == el_fogcolor)
{
XMBAttributeList fogAttributes = fog.GetAttributes();
m_MapReader.m_LightEnv.m_FogColor = RGBColor(
fogAttributes.GetNamedItem(at_r).ToFloat(),
fogAttributes.GetNamedItem(at_g).ToFloat(),
fogAttributes.GetNamedItem(at_b).ToFloat());
}
else if (fog_element_name == el_fogfactor)
{
m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat();
}
else if (fog_element_name == el_fogthickness)
{
m_MapReader.m_LightEnv.m_FogMax = fog.GetText().ToFloat();
}
}
}
else if (element_name == el_postproc)
{
XERO_ITER_EL(element, postproc)
{
int post_element_name = postproc.GetNodeName();
if (post_element_name == el_brightness)
{
m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat();
}
else if (post_element_name == el_contrast)
{
m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat();
}
else if (post_element_name == el_saturation)
{
m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat();
}
else if (post_element_name == el_bloom)
{
m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat();
}
else if (post_element_name == el_posteffect)
{
if (m_MapReader.pPostproc)
m_MapReader.pPostproc->SetPostEffect(postproc.GetText().FromUTF8());
}
}
}
else if (element_name == el_water)
{
XERO_ITER_EL(element, waterbody)
{
ENSURE(waterbody.GetNodeName() == el_waterbody);
XERO_ITER_EL(waterbody, waterelement)
{
int water_element_name = waterelement.GetNodeName();
if (water_element_name == el_height)
{
CmpPtr cmpWaterManager(*m_MapReader.pSimContext, SYSTEM_ENTITY);
ENSURE(cmpWaterManager);
cmpWaterManager->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText()));
continue;
}
// The rest are purely graphical effects, and should be ignored if
// graphics are disabled
if (!m_MapReader.pWaterMan)
continue;
if (water_element_name == el_type)
{
if (waterelement.GetText() == "default")
m_MapReader.pWaterMan->m_WaterType = L"ocean";
else
m_MapReader.pWaterMan->m_WaterType = waterelement.GetText().FromUTF8();
}
#define READ_COLOR(el, out) \
else if (water_element_name == el) \
{ \
XMBAttributeList colorAttrs = waterelement.GetAttributes(); \
out = CColor( \
colorAttrs.GetNamedItem(at_r).ToFloat(), \
colorAttrs.GetNamedItem(at_g).ToFloat(), \
colorAttrs.GetNamedItem(at_b).ToFloat(), \
1.f); \
}
#define READ_FLOAT(el, out) \
else if (water_element_name == el) \
{ \
out = waterelement.GetText().ToFloat(); \
} \
READ_COLOR(el_color, m_MapReader.pWaterMan->m_WaterColor)
READ_COLOR(el_tint, m_MapReader.pWaterMan->m_WaterTint)
READ_FLOAT(el_waviness, m_MapReader.pWaterMan->m_Waviness)
READ_FLOAT(el_murkiness, m_MapReader.pWaterMan->m_Murkiness)
READ_FLOAT(el_windangle, m_MapReader.pWaterMan->m_WindAngle)
#undef READ_FLOAT
#undef READ_COLOR
else
debug_warn(L"Invalid map XML data");
}
}
}
else
debug_warn(L"Invalid map XML data");
}
m_MapReader.m_LightEnv.CalculateSunDirection();
}
void CXMLReader::ReadCamera(XMBElement parent)
{
// defaults if we don't find player starting camera
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(declination);
EL(rotation);
EL(position);
AT(angle);
AT(x); AT(y); AT(z);
#undef AT
#undef EL
float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
CVector3D translation = CVector3D(100, 150, -100);
XERO_ITER_EL(parent, element)
{
int element_name = element.GetNodeName();
XMBAttributeList attrs = element.GetAttributes();
if (element_name == el_declination)
{
declination = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_rotation)
{
rotation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_position)
{
translation = CVector3D(
attrs.GetNamedItem(at_x).ToFloat(),
attrs.GetNamedItem(at_y).ToFloat(),
attrs.GetNamedItem(at_z).ToFloat());
}
else
debug_warn(L"Invalid map XML data");
}
if (m_MapReader.pGameView)
{
m_MapReader.pGameView->GetCamera()->m_Orientation.SetXRotation(declination);
m_MapReader.pGameView->GetCamera()->m_Orientation.RotateY(rotation);
m_MapReader.pGameView->GetCamera()->m_Orientation.Translate(translation);
m_MapReader.pGameView->GetCamera()->UpdateFrustum();
}
}
void CXMLReader::ReadPaths(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(path);
EL(rotation);
EL(node);
EL(position);
EL(target);
AT(name);
AT(timescale);
AT(orientation);
AT(mode);
AT(style);
AT(x);
AT(y);
AT(z);
AT(deltatime);
#undef EL
#undef AT
CmpPtr cmpCinemaManager(*m_MapReader.pSimContext, SYSTEM_ENTITY);
XERO_ITER_EL(parent, element)
{
int elementName = element.GetNodeName();
if (elementName == el_path)
{
CCinemaData pathData;
XMBAttributeList attrs = element.GetAttributes();
CStrW pathName(attrs.GetNamedItem(at_name).FromUTF8());
pathData.m_Name = pathName;
pathData.m_Timescale = fixed::FromString(attrs.GetNamedItem(at_timescale));
pathData.m_Orientation = attrs.GetNamedItem(at_orientation).FromUTF8();
pathData.m_Mode = attrs.GetNamedItem(at_mode).FromUTF8();
pathData.m_Style = attrs.GetNamedItem(at_style).FromUTF8();
TNSpline positionSpline, targetSpline;
fixed lastPositionTime = fixed::Zero();
fixed lastTargetTime = fixed::Zero();
XERO_ITER_EL(element, pathChild)
{
elementName = pathChild.GetNodeName();
attrs = pathChild.GetAttributes();
// Load node data used for spline
if (elementName == el_node)
{
lastPositionTime += fixed::FromString(attrs.GetNamedItem(at_deltatime));
lastTargetTime += fixed::FromString(attrs.GetNamedItem(at_deltatime));
XERO_ITER_EL(pathChild, nodeChild)
{
elementName = nodeChild.GetNodeName();
attrs = nodeChild.GetAttributes();
if (elementName == el_position)
{
CFixedVector3D position(fixed::FromString(attrs.GetNamedItem(at_x)),
fixed::FromString(attrs.GetNamedItem(at_y)),
fixed::FromString(attrs.GetNamedItem(at_z)));
positionSpline.AddNode(position, CFixedVector3D(), lastPositionTime);
lastPositionTime = fixed::Zero();
}
else if (elementName == el_rotation)
{
// TODO: Implement rotation slerp/spline as another object
}
else if (elementName == el_target)
{
CFixedVector3D targetPosition(fixed::FromString(attrs.GetNamedItem(at_x)),
fixed::FromString(attrs.GetNamedItem(at_y)),
fixed::FromString(attrs.GetNamedItem(at_z)));
targetSpline.AddNode(targetPosition, CFixedVector3D(), lastTargetTime);
lastTargetTime = fixed::Zero();
}
else
LOGWARNING("Invalid cinematic element for node child");
}
}
else
LOGWARNING("Invalid cinematic element for path child");
}
// Construct cinema path with data gathered
CCinemaPath path(pathData, positionSpline, targetSpline);
if (path.Empty())
{
LOGWARNING("Path with name '%s' is empty", pathName.ToUTF8());
return;
}
if (!cmpCinemaManager)
continue;
if (!cmpCinemaManager->HasPath(pathName))
cmpCinemaManager->AddPath(path);
else
LOGWARNING("Path with name '%s' already exists", pathName.ToUTF8());
}
else
LOGWARNING("Invalid path child with name '%s'", element.GetText());
}
}
void CXMLReader::ReadTriggers(XMBElement UNUSED(parent))
{
}
int CXMLReader::ReadEntities(XMBElement parent, double end_time)
{
XMBElementList entities = parent.GetChildNodes();
ENSURE(m_MapReader.pSimulation2);
CSimulation2& sim = *m_MapReader.pSimulation2;
CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY);
while (entity_idx < entities.size())
{
// all new state at this scope and below doesn't need to be
// wrapped, since we only yield after a complete iteration.
XMBElement entity = entities[entity_idx++];
ENSURE(entity.GetNodeName() == el_entity);
XMBAttributeList attrs = entity.GetAttributes();
CStr uid = attrs.GetNamedItem(at_uid);
ENSURE(!uid.empty());
int EntityUid = uid.ToInt();
CStrW TemplateName;
int PlayerID = 0;
std::vector Garrison;
std::vector> Turrets;
CFixedVector3D Position;
CFixedVector3D Orientation;
long Seed = -1;
// Obstruction control groups.
entity_id_t ControlGroup = INVALID_ENTITY;
entity_id_t ControlGroup2 = INVALID_ENTITY;
XERO_ITER_EL(entity, setting)
{
int element_name = setting.GetNodeName();
//
if (element_name == el_template)
{
TemplateName = setting.GetText().FromUTF8();
}
//
else if (element_name == el_player)
{
PlayerID = setting.GetText().ToInt();
}
//
else if (element_name == el_position)
{
XMBAttributeList positionAttrs = setting.GetAttributes();
Position = CFixedVector3D(
fixed::FromString(positionAttrs.GetNamedItem(at_x)),
fixed::FromString(positionAttrs.GetNamedItem(at_y)),
fixed::FromString(positionAttrs.GetNamedItem(at_z)));
}
//
else if (element_name == el_orientation)
{
XMBAttributeList orientationAttrs = setting.GetAttributes();
Orientation = CFixedVector3D(
fixed::FromString(orientationAttrs.GetNamedItem(at_x)),
fixed::FromString(orientationAttrs.GetNamedItem(at_y)),
fixed::FromString(orientationAttrs.GetNamedItem(at_z)));
// TODO: what happens if some attributes are missing?
}
//
else if (element_name == el_obstruction)
{
XMBAttributeList obstructionAttrs = setting.GetAttributes();
ControlGroup = obstructionAttrs.GetNamedItem(at_group).ToInt();
ControlGroup2 = obstructionAttrs.GetNamedItem(at_group2).ToInt();
}
//
else if (element_name == el_garrison)
{
XMBElementList garrison = setting.GetChildNodes();
Garrison.reserve(garrison.size());
for (const XMBElement& garr_ent : garrison)
{
XMBAttributeList garrisonAttrs = garr_ent.GetAttributes();
Garrison.push_back(garrisonAttrs.GetNamedItem(at_uid).ToInt());
}
}
//
else if (element_name == el_turrets)
{
XMBElementList turrets = setting.GetChildNodes();
Turrets.reserve(turrets.size());
for (const XMBElement& turretPoint : turrets)
{
XMBAttributeList turretAttrs = turretPoint.GetAttributes();
Turrets.emplace_back(
turretAttrs.GetNamedItem(at_turret),
turretAttrs.GetNamedItem(at_uid).ToInt()
);
}
}
//
else if (element_name == el_actor)
{
XMBAttributeList attrs = setting.GetAttributes();
CStr seedStr = attrs.GetNamedItem(at_seed);
if (!seedStr.empty())
{
Seed = seedStr.ToLong();
ENSURE(Seed >= 0);
}
}
else
debug_warn(L"Invalid map XML data");
}
entity_id_t ent = sim.AddEntity(TemplateName, EntityUid);
entity_id_t player = cmpPlayerManager->GetPlayerByID(PlayerID);
if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
{ // Don't add entities with invalid player IDs
LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(TemplateName));
}
else
{
CmpPtr cmpPosition(sim, ent);
if (cmpPosition)
{
cmpPosition->JumpTo(Position.X, Position.Z);
cmpPosition->SetYRotation(Orientation.Y);
// TODO: other parts of the position
}
if (!Garrison.empty())
{
CmpPtr cmpGarrisonHolder(sim, ent);
if (cmpGarrisonHolder)
cmpGarrisonHolder->SetInitEntities(std::move(Garrison));
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no GarrisonHolder component and thus cannot garrison units.", ent, PlayerID);
}
// Needs to be before ownership changes to prevent initialising
// subunits too soon.
if (!Turrets.empty())
{
CmpPtr cmpTurretHolder(sim, ent);
if (cmpTurretHolder)
cmpTurretHolder->SetInitEntities(std::move(Turrets));
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no TurretHolder component and thus cannot use turrets.", ent, PlayerID);
}
CmpPtr cmpOwnership(sim, ent);
if (cmpOwnership)
cmpOwnership->SetOwner(PlayerID);
CmpPtr cmpObstruction(sim, ent);
if (cmpObstruction)
{
if (ControlGroup != INVALID_ENTITY)
cmpObstruction->SetControlGroup(ControlGroup);
if (ControlGroup2 != INVALID_ENTITY)
cmpObstruction->SetControlGroup2(ControlGroup2);
cmpObstruction->ResolveFoundationCollisions();
}
CmpPtr cmpVisual(sim, ent);
if (cmpVisual)
{
if (Seed != -1)
cmpVisual->SetActorSeed((u32)Seed);
// TODO: variation/selection strings
}
if (PlayerID == m_MapReader.m_PlayerID && (boost::algorithm::ends_with(TemplateName, L"civil_centre") || m_MapReader.m_StartingCameraTarget == INVALID_ENTITY))
{
// Focus on civil centre or first entity owned by player
m_MapReader.m_StartingCameraTarget = ent;
}
}
completed_jobs++;
LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
}
return 0;
}
void CXMLReader::ReadXML()
{
for (XMBElement node : nodes)
{
CStr name = xmb_file.GetElementString(node.GetNodeName());
if (name == "Terrain")
{
ReadTerrain(node);
}
else if (name == "Environment")
{
ReadEnvironment(node);
}
else if (name == "Camera")
{
ReadCamera(node);
}
else if (name == "ScriptSettings")
{
// Already loaded - this is to prevent an assertion
}
else if (name == "Entities")
{
// Handled by ProgressiveReadEntities instead
}
else if (name == "Paths")
{
ReadPaths(node);
}
else if (name == "Triggers")
{
ReadTriggers(node);
}
else if (name == "Script")
{
if (m_MapReader.pSimulation2)
m_MapReader.pSimulation2->SetStartupScript(node.GetText());
}
else
{
debug_printf("Invalid XML element in map file: %s\n", name.c_str());
debug_warn(L"Invalid map XML data");
}
}
}
int CXMLReader::ProgressiveReadEntities()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
int ret;
while (node_idx < nodes.size())
{
XMBElement node = nodes[node_idx];
CStr name = xmb_file.GetElementString(node.GetNodeName());
if (name == "Entities")
{
if (!m_MapReader.m_SkipEntities)
{
ret = ReadEntities(node, end_time);
if (ret != 0) // error or timed out
return ret;
}
}
node_idx++;
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// load script settings from map
int CMapReader::LoadScriptSettings()
{
if (!xml_reader)
xml_reader = new CXMLReader(filename_xml, *this);
// parse the script settings
if (pSimulation2)
pSimulation2->SetMapSettings(xml_reader->ReadScriptSettings());
return 0;
}
// load player settings script
int CMapReader::LoadPlayerSettings()
{
if (pSimulation2)
pSimulation2->LoadPlayerSettings(true);
return 0;
}
// load map settings script
int CMapReader::LoadMapSettings()
{
if (pSimulation2)
pSimulation2->LoadMapSettings();
return 0;
}
int CMapReader::ReadXML()
{
if (!xml_reader)
xml_reader = new CXMLReader(filename_xml, *this);
xml_reader->ReadXML();
return 0;
}
// progressive
int CMapReader::ReadXMLEntities()
{
if (!xml_reader)
xml_reader = new CXMLReader(filename_xml, *this);
int ret = xml_reader->ProgressiveReadEntities();
// finished or failed
if (ret <= 0)
{
SAFE_DELETE(xml_reader);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int CMapReader::LoadRMSettings()
{
// copy random map settings over to sim
ENSURE(pSimulation2);
pSimulation2->SetMapSettings(m_ScriptSettings);
return 0;
}
-int CMapReader::GenerateMap(const CStrW& scriptFile)
+struct CMapReader::GeneratorState
{
- ScriptRequest rq(pSimulation2->GetScriptInterface());
+ std::atomic progress{1};
+ Future task;
- if (!m_MapGen)
+ ~GeneratorState()
{
- // Initialize map generator
- m_MapGen = new CMapGenerator();
+ task.CancelOrWait();
+ }
+};
- VfsPath scriptPath;
+int CMapReader::StartMapGeneration(const CStrW& scriptFile)
+{
+ ScriptRequest rq(pSimulation2->GetScriptInterface());
- if (scriptFile.length())
- scriptPath = L"maps/random/" + scriptFile;
+ m_GeneratorState = std::make_unique();
- // Stringify settings to pass across threads
- std::string scriptSettings = Script::StringifyJSON(rq, &m_ScriptSettings);
+ // The settings are stringified to pass them to the task.
+ m_GeneratorState->task = Threading::TaskManager::Instance().PushTask(
+ [&progress = m_GeneratorState->progress, scriptFile,
+ settings = Script::StringifyJSON(rq, &m_ScriptSettings)]
+ {
+ PROFILE2("Map Generation");
- // Try to generate map
- m_MapGen->GenerateMap(scriptPath, scriptSettings);
- }
+ const CStrW scriptPath{scriptFile.empty() ? L"" : L"maps/random/" + scriptFile};
- // Check status
- int progress = m_MapGen->GetProgress();
- if (progress < 0)
- {
- // RMS failed - return to main menu
- throw PSERROR_Game_World_MapLoadFailed("Error generating random map.\nCheck application log for details.");
- }
- else if (progress == 0)
- {
- // Finished, get results as StructuredClone object, which must be read to obtain the JS::Value
- Script::StructuredClone results = m_MapGen->GetResults();
+ const std::shared_ptr mapgenContext{ScriptContext::CreateContext(
+ MAP_GENERATION_CONTEXT_SIZE)};
+ ScriptInterface mapgenInterface{"Engine", "MapGenerator", mapgenContext};
- // Parse data into simulation context
- JS::RootedValue data(rq.cx);
- Script::ReadStructuredClone(rq, results, &data);
+ return RunMapGenerationScript(progress, mapgenInterface, scriptPath, settings);
+ });
- if (data.isUndefined())
- {
- // RMS failed - return to main menu
- throw PSERROR_Game_World_MapLoadFailed("Error generating random map.\nCheck application log for details.");
- }
- else
- {
- m_MapData.init(rq.cx, data);
- }
- }
+ return 0;
+}
+
+[[noreturn]] void ThrowMapGenerationError()
+{
+ throw PSERROR_Game_World_MapLoadFailed{
+ "Error generating random map.\nCheck application log for details."};
+};
- // return progress
- return progress;
+int CMapReader::PollMapGeneration()
+{
+ ENSURE(m_GeneratorState);
+
+ if (!m_GeneratorState->task.IsReady())
+ return m_GeneratorState->progress.load();
+
+ const Script::StructuredClone results{m_GeneratorState->task.Get()};
+ if (!results)
+ ThrowMapGenerationError();
+
+ // Parse data into simulation context
+ ScriptRequest rq(pSimulation2->GetScriptInterface());
+ JS::RootedValue data{rq.cx};
+ Script::ReadStructuredClone(rq, results, &data);
+
+ if (data.isUndefined())
+ ThrowMapGenerationError();
+
+ m_MapData.init(rq.cx, data);
+
+ return 0;
};
int CMapReader::ParseTerrain()
{
TIMER(L"ParseTerrain");
ScriptRequest rq(pSimulation2->GetScriptInterface());
// parse terrain from map data
// an error here should stop the loading process
#define GET_TERRAIN_PROPERTY(val, prop, out)\
if (!Script::GetProperty(rq, val, #prop, out))\
{ LOGERROR("CMapReader::ParseTerrain() failed to get '%s' property", #prop);\
throw PSERROR_Game_World_MapLoadFailed("Error parsing terrain data.\nCheck application log for details"); }
u32 size;
GET_TERRAIN_PROPERTY(m_MapData, size, size)
m_PatchesPerSide = size / PATCH_SIZE;
// flat heightmap of u16 data
GET_TERRAIN_PROPERTY(m_MapData, height, m_Heightmap)
// load textures
std::vector textureNames;
GET_TERRAIN_PROPERTY(m_MapData, textureNames, textureNames)
num_terrain_tex = textureNames.size();
while (cur_terrain_tex < num_terrain_tex)
{
if (CTerrainTextureManager::IsInitialised())
{
CTerrainTextureEntry* texentry = g_TexMan.FindTexture(textureNames[cur_terrain_tex]);
m_TerrainTextures.push_back(texentry);
}
cur_terrain_tex++;
}
// build tile data
m_Tiles.resize(SQR(size));
JS::RootedValue tileData(rq.cx);
GET_TERRAIN_PROPERTY(m_MapData, tileData, &tileData)
// parse tile data object into flat arrays
std::vector tileIndex;
std::vector tilePriority;
GET_TERRAIN_PROPERTY(tileData, index, tileIndex);
GET_TERRAIN_PROPERTY(tileData, priority, tilePriority);
ENSURE(SQR(size) == tileIndex.size() && SQR(size) == tilePriority.size());
// reorder by patches and store
for (size_t x = 0; x < size; ++x)
{
size_t patchX = x / PATCH_SIZE;
size_t offX = x % PATCH_SIZE;
for (size_t y = 0; y < size; ++y)
{
size_t patchY = y / PATCH_SIZE;
size_t offY = y % PATCH_SIZE;
STileDesc tile;
tile.m_Tex1Index = tileIndex[y*size + x];
tile.m_Tex2Index = 0xFFFF;
tile.m_Priority = tilePriority[y*size + x];
m_Tiles[(patchY * m_PatchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)] = tile;
}
}
// reset generator state
cur_terrain_tex = 0;
#undef GET_TERRAIN_PROPERTY
return 0;
}
int CMapReader::ParseEntities()
{
TIMER(L"ParseEntities");
ScriptRequest rq(pSimulation2->GetScriptInterface());
// parse entities from map data
std::vector entities;
if (!Script::GetProperty(rq, m_MapData, "entities", entities))
LOGWARNING("CMapReader::ParseEntities() failed to get 'entities' property");
CSimulation2& sim = *pSimulation2;
CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY);
size_t entity_idx = 0;
size_t num_entities = entities.size();
Entity currEnt;
while (entity_idx < num_entities)
{
// Get current entity struct
currEnt = entities[entity_idx];
entity_id_t ent = pSimulation2->AddEntity(currEnt.templateName, currEnt.entityID);
entity_id_t player = cmpPlayerManager->GetPlayerByID(currEnt.playerID);
if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
{ // Don't add entities with invalid player IDs
LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(currEnt.templateName));
}
else
{
CmpPtr cmpPosition(sim, ent);
if (cmpPosition)
{
cmpPosition->JumpTo(currEnt.position.X * (int)TERRAIN_TILE_SIZE, currEnt.position.Z * (int)TERRAIN_TILE_SIZE);
cmpPosition->SetYRotation(currEnt.rotation.Y);
// TODO: other parts of the position
}
CmpPtr cmpOwnership(sim, ent);
if (cmpOwnership)
cmpOwnership->SetOwner(currEnt.playerID);
// Detect and fix collisions between foundation-blocking entities.
// This presently serves to copy wall tower control groups to wall
// segments, allowing players to expand RMS-generated walls.
CmpPtr cmpObstruction(sim, ent);
if (cmpObstruction)
cmpObstruction->ResolveFoundationCollisions();
if (currEnt.playerID == m_PlayerID && (boost::algorithm::ends_with(currEnt.templateName, L"civil_centre") || m_StartingCameraTarget == INVALID_ENTITY))
{
// Focus on civil centre or first entity owned by player
m_StartingCameraTarget = currEnt.entityID;
}
}
entity_idx++;
}
return 0;
}
int CMapReader::ParseEnvironment()
{
// parse environment settings from map data
ScriptRequest rq(pSimulation2->GetScriptInterface());
#define GET_ENVIRONMENT_PROPERTY(val, prop, out)\
if (!Script::GetProperty(rq, val, #prop, out))\
LOGWARNING("CMapReader::ParseEnvironment() failed to get '%s' property", #prop);
JS::RootedValue envObj(rq.cx);
GET_ENVIRONMENT_PROPERTY(m_MapData, Environment, &envObj)
if (envObj.isUndefined())
{
LOGWARNING("CMapReader::ParseEnvironment(): Environment settings not found");
return 0;
}
if (pPostproc)
pPostproc->SetPostEffect(L"default");
std::wstring skySet;
GET_ENVIRONMENT_PROPERTY(envObj, SkySet, skySet)
if (pSkyMan)
pSkyMan->SetSkySet(skySet);
CColor sunColor;
GET_ENVIRONMENT_PROPERTY(envObj, SunColor, sunColor)
m_LightEnv.m_SunColor = RGBColor(sunColor.r, sunColor.g, sunColor.b);
GET_ENVIRONMENT_PROPERTY(envObj, SunElevation, m_LightEnv.m_Elevation)
GET_ENVIRONMENT_PROPERTY(envObj, SunRotation, m_LightEnv.m_Rotation)
CColor ambientColor;
GET_ENVIRONMENT_PROPERTY(envObj, AmbientColor, ambientColor)
m_LightEnv.m_AmbientColor = RGBColor(ambientColor.r, ambientColor.g, ambientColor.b);
// Water properties
JS::RootedValue waterObj(rq.cx);
GET_ENVIRONMENT_PROPERTY(envObj, Water, &waterObj)
JS::RootedValue waterBodyObj(rq.cx);
GET_ENVIRONMENT_PROPERTY(waterObj, WaterBody, &waterBodyObj)
// Water level - necessary
float waterHeight;
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Height, waterHeight)
CmpPtr cmpWaterManager(*pSimulation2, SYSTEM_ENTITY);
ENSURE(cmpWaterManager);
cmpWaterManager->SetWaterLevel(entity_pos_t::FromFloat(waterHeight));
// If we have graphics, get rest of settings
if (pWaterMan)
{
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Type, pWaterMan->m_WaterType)
if (pWaterMan->m_WaterType == L"default")
pWaterMan->m_WaterType = L"ocean";
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Color, pWaterMan->m_WaterColor)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Tint, pWaterMan->m_WaterTint)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Waviness, pWaterMan->m_Waviness)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Murkiness, pWaterMan->m_Murkiness)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, WindAngle, pWaterMan->m_WindAngle)
}
JS::RootedValue fogObject(rq.cx);
GET_ENVIRONMENT_PROPERTY(envObj, Fog, &fogObject);
GET_ENVIRONMENT_PROPERTY(fogObject, FogFactor, m_LightEnv.m_FogFactor);
GET_ENVIRONMENT_PROPERTY(fogObject, FogThickness, m_LightEnv.m_FogMax);
CColor fogColor;
GET_ENVIRONMENT_PROPERTY(fogObject, FogColor, fogColor);
m_LightEnv.m_FogColor = RGBColor(fogColor.r, fogColor.g, fogColor.b);
JS::RootedValue postprocObject(rq.cx);
GET_ENVIRONMENT_PROPERTY(envObj, Postproc, &postprocObject);
std::wstring postProcEffect;
GET_ENVIRONMENT_PROPERTY(postprocObject, PostprocEffect, postProcEffect);
if (pPostproc)
pPostproc->SetPostEffect(postProcEffect);
GET_ENVIRONMENT_PROPERTY(postprocObject, Brightness, m_LightEnv.m_Brightness);
GET_ENVIRONMENT_PROPERTY(postprocObject, Contrast, m_LightEnv.m_Contrast);
GET_ENVIRONMENT_PROPERTY(postprocObject, Saturation, m_LightEnv.m_Saturation);
GET_ENVIRONMENT_PROPERTY(postprocObject, Bloom, m_LightEnv.m_Bloom);
m_LightEnv.CalculateSunDirection();
#undef GET_ENVIRONMENT_PROPERTY
return 0;
}
int CMapReader::ParseCamera()
{
ScriptRequest rq(pSimulation2->GetScriptInterface());
// parse camera settings from map data
// defaults if we don't find player starting camera
float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
CVector3D translation = CVector3D(100, 150, -100);
#define GET_CAMERA_PROPERTY(val, prop, out)\
if (!Script::GetProperty(rq, val, #prop, out))\
LOGWARNING("CMapReader::ParseCamera() failed to get '%s' property", #prop);
JS::RootedValue cameraObj(rq.cx);
GET_CAMERA_PROPERTY(m_MapData, Camera, &cameraObj)
if (!cameraObj.isUndefined())
{ // If camera property exists, read values
CFixedVector3D pos;
GET_CAMERA_PROPERTY(cameraObj, Position, pos)
translation = pos;
GET_CAMERA_PROPERTY(cameraObj, Rotation, rotation)
GET_CAMERA_PROPERTY(cameraObj, Declination, declination)
}
#undef GET_CAMERA_PROPERTY
if (pGameView)
{
pGameView->GetCamera()->m_Orientation.SetXRotation(declination);
pGameView->GetCamera()->m_Orientation.RotateY(rotation);
pGameView->GetCamera()->m_Orientation.Translate(translation);
pGameView->GetCamera()->UpdateFrustum();
}
return 0;
}
CMapReader::~CMapReader()
{
// Cleaup objects
delete xml_reader;
- delete m_MapGen;
}
Index: ps/trunk/source/graphics/MapReader.h
===================================================================
--- ps/trunk/source/graphics/MapReader.h (revision 27943)
+++ ps/trunk/source/graphics/MapReader.h (revision 27944)
@@ -1,177 +1,181 @@
/* Copyright (C) 2023 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 .
*/
#ifndef INCLUDED_MAPREADER
#define INCLUDED_MAPREADER
#include "MapIO.h"
#include "graphics/LightEnv.h"
#include "ps/CStr.h"
#include "ps/FileIo.h"
#include "scriptinterface/ScriptTypes.h"
#include "simulation2/system/Entity.h"
+#include
+
class CTerrain;
class WaterManager;
class SkyManager;
class CLightEnv;
class CCinemaManager;
class CPostprocManager;
class CTriggerManager;
class CSimulation2;
class CSimContext;
class CTerrainTextureEntry;
class CGameView;
class CXMLReader;
-class CMapGenerator;
class ScriptContext;
class ScriptInterface;
class CMapReader : public CMapIO
{
friend class CXMLReader;
public:
// constructor
CMapReader();
~CMapReader();
// LoadMap: try to load the map from given file; reinitialise the scene to new data if successful
void LoadMap(const VfsPath& pathname, const ScriptContext& cx, JS::HandleValue settings, CTerrain*, WaterManager*, SkyManager*, CLightEnv*, CGameView*,
CCinemaManager*, CTriggerManager*, CPostprocManager* pPostproc, CSimulation2*, const CSimContext*,
int playerID, bool skipEntities);
void LoadRandomMap(const CStrW& scriptFile, const ScriptContext& cx, JS::HandleValue settings, CTerrain*, WaterManager*, SkyManager*, CLightEnv*, CGameView*, CCinemaManager*, CTriggerManager*, CPostprocManager* pPostproc_, CSimulation2*, int playerID);
private:
// Load script settings for use by scripts
int LoadScriptSettings();
// load player settings only
int LoadPlayerSettings();
// load map settings only
int LoadMapSettings();
// UnpackTerrain: unpack the terrain from the input stream
int UnpackTerrain();
// UnpackCinema: unpack the cinematic tracks from the input stream
int UnpackCinema();
// ApplyData: take all the input data, and rebuild the scene from it
int ApplyData();
int ApplyTerrainData();
// read some misc data from the XML file
int ReadXML();
// read entity data from the XML file
int ReadXMLEntities();
// Copy random map settings over to sim
int LoadRMSettings();
// Generate random map
- int GenerateMap(const CStrW& scriptFile);
+ int StartMapGeneration(const CStrW& scriptFile);
+ int PollMapGeneration();
// Parse script data into terrain
int ParseTerrain();
// Parse script data into entities
int ParseEntities();
// Parse script data into environment
int ParseEnvironment();
// Parse script data into camera
int ParseCamera();
// size of map
- ssize_t m_PatchesPerSide;
+ ssize_t m_PatchesPerSide{0};
// heightmap for map
std::vector m_Heightmap;
// list of terrain textures used by map
std::vector m_TerrainTextures;
// tile descriptions for each tile
std::vector m_Tiles;
// lightenv stored in file
CLightEnv m_LightEnv;
// startup script
CStrW m_Script;
// random map data
JS::PersistentRootedValue m_ScriptSettings;
JS::PersistentRootedValue m_MapData;
- CMapGenerator* m_MapGen;
+ struct GeneratorState;
+ std::unique_ptr m_GeneratorState;
CFileUnpacker unpacker;
CTerrain* pTerrain;
WaterManager* pWaterMan;
SkyManager* pSkyMan;
CPostprocManager* pPostproc;
CLightEnv* pLightEnv;
CGameView* pGameView;
CCinemaManager* pCinema;
CTriggerManager* pTrigMan;
CSimulation2* pSimulation2;
const CSimContext* pSimContext;
int m_PlayerID;
bool m_SkipEntities;
VfsPath filename_xml;
bool only_xml;
u32 file_format_version;
entity_id_t m_StartingCameraTarget;
CVector3D m_StartingCamera;
// UnpackTerrain generator state
- size_t cur_terrain_tex;
+ // It's important to initialize it to 0 - resets generator state
+ size_t cur_terrain_tex{0};
size_t num_terrain_tex;
- CXMLReader* xml_reader;
+ CXMLReader* xml_reader{nullptr};
};
/**
* A restricted map reader that returns various summary information
* for use by scripts (particularly the GUI).
*/
class CMapSummaryReader
{
public:
/**
* Try to load a map file.
* @param pathname Path to .pmp or .xml file
*/
PSRETURN LoadMap(const VfsPath& pathname);
/**
* Returns a value of the form:
* @code
* {
* "settings": { ... contents of the map's ... }
* }
* @endcode
*/
void GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue);
private:
CStr m_ScriptSettings;
};
#endif
Index: ps/trunk/source/graphics/tests/test_MapGenerator.h
===================================================================
--- ps/trunk/source/graphics/tests/test_MapGenerator.h (revision 27943)
+++ ps/trunk/source/graphics/tests/test_MapGenerator.h (revision 27944)
@@ -1,60 +1,69 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2023 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 "graphics/MapGenerator.h"
#include "ps/Filesystem.h"
#include "simulation2/system/ComponentTest.h"
+#include
+
class TestMapGenerator : public CxxTest::TestSuite
{
public:
void setUp()
{
g_VFS = CreateVfs();
g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST);
g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors
CXeromyces::Startup();
}
void tearDown()
{
CXeromyces::Terminate();
g_VFS.reset();
}
void test_mapgen_scripts()
{
if (!VfsDirectoryExists(L"maps/random/tests/"))
{
debug_printf("Skipping map generator tests (can't find binaries/data/mods/public/maps/random/tests/)\n");
return;
}
VfsPaths paths;
TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"maps/random/tests/", L"test_*.js", paths));
for (const VfsPath& path : paths)
{
ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext);
ScriptTestSetup(scriptInterface);
- CMapGeneratorWorker worker(&scriptInterface);
- worker.InitScriptInterface(0);
- scriptInterface.LoadGlobalScriptFile(path);
+ // It's never read in the test so it doesn't matter to what value it's initialized. For
+ // good practice it's initialized to 1.
+ std::atomic progress{1};
+
+ const Script::StructuredClone result{RunMapGenerationScript(progress, scriptInterface,
+ path, "{\"Seed\": 0}", JSPROP_ENUMERATE | JSPROP_PERMANENT)};
+
+ // The test scripts don't call `ExportMap` so `RunMapGenerationScript` allways returns
+ // `nullptr`.
+ TS_ASSERT_EQUALS(result, nullptr);
}
}
};
Index: ps/trunk/source/ps/scripting/JSInterface_VFS.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 27943)
+++ ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 27944)
@@ -1,315 +1,318 @@
/* Copyright (C) 2023 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 "JSInterface_VFS.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Filesystem.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/JSON.h"
#include "scriptinterface/Object.h"
#include
namespace JSI_VFS
{
// Only allow engine compartments to read files they may be concerned about.
#define PathRestriction_GUI {L"gui/", L"simulation/", L"maps/", L"campaigns/", L"saves/campaigns/", L"config/matchsettings.json", L"config/matchsettings.mp.json", L"moddata"}
#define PathRestriction_Simulation {L"simulation/"}
#define PathRestriction_Maps {L"simulation/", L"maps/"}
// shared error handling code
#define JS_CHECK_FILE_ERR(err)\
/* this is liable to happen often, so don't complain */\
if (err == ERR::VFS_FILE_NOT_FOUND)\
{\
return 0; \
}\
/* unknown failure. We output an error message. */\
else if (err < 0)\
LOGERROR("Unknown failure in VFS %i", err );
/* else: success */
// Tests whether the current script context is allowed to read from the given directory
bool PathRestrictionMet(const ScriptRequest& rq, const std::vector& validPaths, const CStrW& filePath)
{
for (const CStrW& validPath : validPaths)
if (filePath.find(validPath) == 0)
return true;
CStrW allowedPaths;
for (std::size_t i = 0; i < validPaths.size(); ++i)
{
if (i != 0)
allowedPaths += L", ";
allowedPaths += L"\"" + validPaths[i] + L"\"";
}
ScriptException::Raise(rq, "Restricted access to %s. This part of the engine may only read from %s!", utf8_from_wstring(filePath).c_str(), utf8_from_wstring(allowedPaths).c_str());
return false;
}
// state held across multiple BuildDirEntListCB calls; init by BuildDirEntList.
struct BuildDirEntListState
{
const ScriptRequest& rq;
JS::PersistentRootedObject filename_array;
int cur_idx;
BuildDirEntListState(const ScriptRequest& rq)
: rq(rq),
filename_array(rq.cx),
cur_idx(0)
{
filename_array = JS::NewArrayObject(rq.cx, JS::HandleValueArray::empty());
}
};
// called for each matching directory entry; add its full pathname to array.
static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData)
{
BuildDirEntListState* s = (BuildDirEntListState*)cbData;
JS::RootedObject filenameArrayObj(s->rq.cx, s->filename_array);
JS::RootedValue val(s->rq.cx);
Script::ToJSVal(s->rq, &val, CStrW(pathname.string()) );
JS_SetElement(s->rq.cx, filenameArrayObj, s->cur_idx++, val);
return INFO::OK;
}
// Return an array of pathname strings, one for each matching entry in the
// specified directory.
// filter_string: default "" matches everything; otherwise, see vfs_next_dirent.
// recurse: should subdirectories be included in the search? default false.
JS::Value BuildDirEntList(const ScriptRequest& rq, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse)
{
if (!PathRestrictionMet(rq, validPaths, path))
return JS::NullValue();
// convert to const wchar_t*; if there's no filter, pass 0 for speed
// (interpreted as: "accept all files without comparing").
const wchar_t* filter = 0;
if (!filterStr.empty())
filter = filterStr.c_str();
int flags = recurse ? vfs::DIR_RECURSIVE : 0;
// build array in the callback function
BuildDirEntListState state(rq);
vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags);
return JS::ObjectValue(*state.filename_array);
}
// Return true iff the file exits
bool FileExists(const ScriptRequest& rq, const std::vector& validPaths, const CStrW& filename)
{
return PathRestrictionMet(rq, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK;
}
// Return time [seconds since 1970] of the last modification to the specified file.
double GetFileMTime(const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (double)fileInfo.MTime();
}
// Return current size of file.
unsigned int GetFileSize(const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (unsigned int)fileInfo.Size();
}
// Return file contents in a string. Assume file is UTF-8 encoded text.
JS::Value ReadFile(const ScriptRequest& rq, const std::vector& validPaths, const CStrW& filename)
{
if (!PathRestrictionMet(rq, validPaths, filename))
return JS::NullValue();
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// Decode as UTF-8
JS::RootedValue ret(rq.cx);
Script::ToJSVal(rq, &ret, contents.FromUTF8());
return ret;
}
// Return file contents as an array of lines. Assume file is UTF-8 encoded text.
JS::Value ReadFileLines(const ScriptRequest& rq, const std::vector& validPaths, const CStrW& filename)
{
if (!PathRestrictionMet(rq, validPaths, filename))
return JS::NullValue();
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// split into array of strings (one per line)
std::stringstream ss(contents);
JS::RootedValue line_array(rq.cx);
Script::CreateArray(rq, &line_array);
std::string line;
int cur_line = 0;
while (std::getline(ss, line))
{
// Decode each line as UTF-8
JS::RootedValue val(rq.cx);
Script::ToJSVal(rq, &val, CStr(line).FromUTF8());
Script::SetPropertyInt(rq, line_array, cur_line++, val);
}
return line_array;
}
// Return file contents parsed as a JS Object
JS::Value ReadJSONFile(const ScriptInterface& scriptInterface, const std::vector& validPaths, const CStrW& filePath)
{
ScriptRequest rq(scriptInterface);
if (!PathRestrictionMet(rq, validPaths, filePath))
return JS::NullValue();
JS::RootedValue out(rq.cx);
Script::ReadJSONFile(rq, filePath, &out);
return out;
}
// Save given JS Object to a JSON file
void WriteJSONFile(const ScriptInterface& scriptInterface, const std::vector& validPaths, const CStrW& filePath, JS::HandleValue val1)
{
ScriptRequest rq(scriptInterface);
if (!PathRestrictionMet(rq, validPaths, filePath))
return;
// TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
JS::RootedValue val(rq.cx, val1);
std::string str(Script::StringifyJSON(rq, &val, false));
VfsPath path(filePath);
WriteBuffer buf;
buf.Append(str.c_str(), str.length());
if (g_VFS->CreateFile(path, buf.Data(), buf.Size()) == INFO::OK)
{
OsPath realPath;
g_VFS->GetRealPath(path, realPath, false);
debug_printf("FILES| JSON data written to '%s'\n", realPath.string8().c_str());
}
else
debug_printf("FILES| Failed to write JSON data to '%s'\n", path.string8().c_str());
}
bool DeleteCampaignSave(const CStrW& filePath)
{
OsPath realPath;
if (filePath.Left(16) != L"saves/campaigns/" || filePath.Right(12) != L".0adcampaign")
return false;
return VfsFileExists(filePath) &&
g_VFS->GetRealPath(filePath, realPath) == INFO::OK &&
g_VFS->RemoveFile(filePath) == INFO::OK &&
wunlink(realPath) == 0;
}
#define VFS_ScriptFunctions(context)\
JS::Value Script_ReadJSONFile_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
{\
return ReadJSONFile(scriptInterface, PathRestriction_##context, filePath);\
}\
void Script_WriteJSONFile_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath, JS::HandleValue val1)\
{\
return WriteJSONFile(scriptInterface, PathRestriction_##context, filePath, val1);\
}\
JS::Value Script_ReadFile_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
{\
return ReadFile(scriptInterface, PathRestriction_##context, filePath);\
}\
JS::Value Script_ReadFileLines_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
{\
return ReadFileLines(scriptInterface, PathRestriction_##context, filePath);\
}\
JS::Value Script_ListDirectoryFiles_##context(const ScriptInterface& scriptInterface, const std::wstring& path, const std::wstring& filterStr, bool recurse)\
{\
return BuildDirEntList(scriptInterface, PathRestriction_##context, path, filterStr, recurse);\
}\
bool Script_FileExists_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
{\
return FileExists(scriptInterface, PathRestriction_##context, filePath);\
}\
VFS_ScriptFunctions(GUI);
VFS_ScriptFunctions(Simulation);
VFS_ScriptFunctions(Maps);
#undef VFS_ScriptFunctions
-void RegisterScriptFunctions_ReadWriteAnywhere(const ScriptRequest& rq)
+void RegisterScriptFunctions_ReadWriteAnywhere(const ScriptRequest& rq,
+ const u16 flags /*= JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT */)
{
- ScriptFunction::Register<&Script_ListDirectoryFiles_GUI>(rq, "ListDirectoryFiles");
- ScriptFunction::Register<&Script_FileExists_GUI>(rq, "FileExists");
- ScriptFunction::Register<&GetFileMTime>(rq, "GetFileMTime");
- ScriptFunction::Register<&GetFileSize>(rq, "GetFileSize");
- ScriptFunction::Register<&Script_ReadFile_GUI>(rq, "ReadFile");
- ScriptFunction::Register<&Script_ReadFileLines_GUI>(rq, "ReadFileLines");
- ScriptFunction::Register<&Script_ReadJSONFile_GUI>(rq, "ReadJSONFile");
- ScriptFunction::Register<&Script_WriteJSONFile_GUI>(rq, "WriteJSONFile");
- ScriptFunction::Register<&DeleteCampaignSave>(rq, "DeleteCampaignSave");
-}
-
-void RegisterScriptFunctions_ReadOnlySimulation(const ScriptRequest& rq)
-{
- ScriptFunction::Register<&Script_ListDirectoryFiles_Simulation>(rq, "ListDirectoryFiles");
- ScriptFunction::Register<&Script_FileExists_Simulation>(rq, "FileExists");
- ScriptFunction::Register<&Script_ReadJSONFile_Simulation>(rq, "ReadJSONFile");
-}
-
-void RegisterScriptFunctions_ReadOnlySimulationMaps(const ScriptRequest& rq)
-{
- ScriptFunction::Register<&Script_ListDirectoryFiles_Maps>(rq, "ListDirectoryFiles");
- ScriptFunction::Register<&Script_FileExists_Maps>(rq, "FileExists");
- ScriptFunction::Register<&Script_ReadJSONFile_Maps>(rq, "ReadJSONFile");
+ ScriptFunction::Register<&Script_ListDirectoryFiles_GUI>(rq, "ListDirectoryFiles", flags);
+ ScriptFunction::Register<&Script_FileExists_GUI>(rq, "FileExists", flags);
+ ScriptFunction::Register<&GetFileMTime>(rq, "GetFileMTime", flags);
+ ScriptFunction::Register<&GetFileSize>(rq, "GetFileSize", flags);
+ ScriptFunction::Register<&Script_ReadFile_GUI>(rq, "ReadFile", flags);
+ ScriptFunction::Register<&Script_ReadFileLines_GUI>(rq, "ReadFileLines", flags);
+ ScriptFunction::Register<&Script_ReadJSONFile_GUI>(rq, "ReadJSONFile", flags);
+ ScriptFunction::Register<&Script_WriteJSONFile_GUI>(rq, "WriteJSONFile", flags);
+ ScriptFunction::Register<&DeleteCampaignSave>(rq, "DeleteCampaignSave", flags);
+}
+
+void RegisterScriptFunctions_ReadOnlySimulation(const ScriptRequest& rq,
+ const u16 flags /*= JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT */)
+{
+ ScriptFunction::Register<&Script_ListDirectoryFiles_Simulation>(rq, "ListDirectoryFiles", flags);
+ ScriptFunction::Register<&Script_FileExists_Simulation>(rq, "FileExists", flags);
+ ScriptFunction::Register<&Script_ReadJSONFile_Simulation>(rq, "ReadJSONFile", flags);
+}
+
+void RegisterScriptFunctions_ReadOnlySimulationMaps(const ScriptRequest& rq,
+ const u16 flags /*= JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT */)
+{
+ ScriptFunction::Register<&Script_ListDirectoryFiles_Maps>(rq, "ListDirectoryFiles", flags);
+ ScriptFunction::Register<&Script_FileExists_Maps>(rq, "FileExists", flags);
+ ScriptFunction::Register<&Script_ReadJSONFile_Maps>(rq, "ReadJSONFile", flags);
}
}
Index: ps/trunk/source/ps/scripting/JSInterface_VFS.h
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_VFS.h (revision 27943)
+++ ps/trunk/source/ps/scripting/JSInterface_VFS.h (revision 27944)
@@ -1,30 +1,33 @@
-/* Copyright (C) 2022 Wildfire Games.
+/* Copyright (C) 2023 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 .
*/
#ifndef INCLUDED_JSI_VFS
#define INCLUDED_JSI_VFS
class ScriptRequest;
namespace JSI_VFS
{
- void RegisterScriptFunctions_ReadWriteAnywhere(const ScriptRequest& rq);
- void RegisterScriptFunctions_ReadOnlySimulation(const ScriptRequest& rq);
- void RegisterScriptFunctions_ReadOnlySimulationMaps(const ScriptRequest& rq);
+ void RegisterScriptFunctions_ReadWriteAnywhere(const ScriptRequest& rq,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
+ void RegisterScriptFunctions_ReadOnlySimulation(const ScriptRequest& rq,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
+ void RegisterScriptFunctions_ReadOnlySimulationMaps(const ScriptRequest& rq,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
}
#endif // INCLUDED_JSI_VFS
Index: ps/trunk/source/scriptinterface/FunctionWrapper.h
===================================================================
--- ps/trunk/source/scriptinterface/FunctionWrapper.h (revision 27943)
+++ ps/trunk/source/scriptinterface/FunctionWrapper.h (revision 27944)
@@ -1,388 +1,392 @@
/* Copyright (C) 2023 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 .
*/
#ifndef INCLUDED_FUNCTIONWRAPPER
#define INCLUDED_FUNCTIONWRAPPER
#include "ScriptConversions.h"
#include "ScriptExceptions.h"
#include "ScriptRequest.h"
#include
#include
#include
class ScriptInterface;
/**
* This class introduces templates to conveniently wrap C++ functions in JSNative functions.
* This _is_ rather template heavy, so compilation times beware.
* The C++ code can have arbitrary arguments and arbitrary return types, so long
* as they can be converted to/from JS using Script::ToJSVal (FromJSVal respectively),
* and they are default-constructible (TODO: that can probably changed).
* (This could be a namespace, but I like being able to specify public/private).
*/
class ScriptFunction
{
private:
ScriptFunction() = delete;
ScriptFunction(const ScriptFunction&) = delete;
ScriptFunction(ScriptFunction&&) = delete;
/**
* In JS->C++ calls, types are converted using FromJSVal,
* and this requires them to be default-constructible (as that function takes an out parameter)
* thus constref needs to be removed when defining the tuple.
* Exceptions are:
* - const ScriptRequest& (as the first argument only, for implementation simplicity).
* - const ScriptInterface& (as the first argument only, for implementation simplicity).
* - JS::HandleValue
*/
template
using type_transform = std::conditional_t<
std::is_same_v || std::is_same_v,
T,
std::remove_const_t>
>;
/**
* Convenient struct to get info on a [class] [const] function pointer.
* TODO VS19: I ran into a really weird bug with an auto specialisation on this taking function pointers.
* It'd be good to add it back once we upgrade.
*/
template struct args_info;
template
struct args_info
{
static constexpr const size_t nb_args = sizeof...(Types);
using return_type = R;
using object_type = void;
using arg_types = std::tuple...>;
};
template
struct args_info : public args_info { using object_type = C; };
template
struct args_info : public args_info {};
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
/**
* DoConvertFromJS takes a type, a JS argument, and converts.
* The type T must be default constructible (except for HandleValue, which is handled specially).
* (possible) TODO: this could probably be changed if FromJSVal had a different signature.
* @param wentOk - true if the conversion succeeded and wentOk was true before, false otherwise.
*/
template
static T DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& wentOk)
{
// No need to convert JS values.
if constexpr (std::is_same_v)
{
// Default-construct values that aren't passed by JS.
// TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky.
if (idx >= args.length())
return JS::UndefinedHandleValue;
else
{
// GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch.
UNUSED2(rq); UNUSED2(args); UNUSED2(wentOk);
return args[idx]; // This passes the null handle value if idx is beyond the length of args.
}
}
else
{
// Default-construct values that aren't passed by JS.
// TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky.
if (idx >= args.length())
return {};
else
{
T ret;
wentOk &= Script::FromJSVal(rq, args[idx], ret);
return ret;
}
}
}
/**
* Wrapper: calls DoConvertFromJS for each element in T.
*/
template
static std::tuple DoConvertFromJS(std::index_sequence, const ScriptRequest& rq,
JS::CallArgs& args, bool& wentOk)
{
return {DoConvertFromJS(rq, args, wentOk)...};
}
/**
* ConvertFromJS is a wrapper around DoConvertFromJS, and handles specific cases for the
* first argument (ScriptRequest, ...).
*
* Trick: to unpack the types of the tuple as a parameter pack, we deduce them from the function signature.
* To do that, we want the tuple in the arguments, but we don't want to actually have to default-instantiate,
* so we'll pass a nullptr that's static_cast to what we want.
*/
template
static std::tuple ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& wentOk,
std::tuple*)
{
return DoConvertFromJS(std::index_sequence_for(), rq, args, wentOk);
}
// Overloads for ScriptRequest& first argument.
template
static std::tuple ConvertFromJS(const ScriptRequest& rq,
JS::CallArgs& args, bool& wentOk, std::tuple*)
{
return std::tuple_cat(std::tie(rq), DoConvertFromJS(
std::index_sequence_for(), rq, args, wentOk));
}
// Overloads for ScriptInterface& first argument.
template
static std::tuple ConvertFromJS(const ScriptRequest& rq,
JS::CallArgs& args, bool& wentOk, std::tuple*)
{
return std::tuple_cat(std::tie(rq.GetScriptInterface()),
DoConvertFromJS(std::index_sequence_for(), rq, args, wentOk));
}
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
/**
* Wrap std::apply for the case where we have an object method or a regular function.
*/
template
static typename args_info::return_type call(T* object, tuple& args)
{
if constexpr(std::is_same_v)
{
// GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch.
UNUSED2(object);
return std::apply(callable, args);
}
else
return std::apply(callable, std::tuple_cat(std::forward_as_tuple(*object), args));
}
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
struct IgnoreResult_t {};
static inline IgnoreResult_t IgnoreResult;
/**
* Converts any number of arguments to a `JS::MutableHandleValueVector`.
* If `idx` is empty this function does nothing. For that case there is a
* `[[maybe_unused]]` on `argv`. GCC would issue a
* "-Wunused-but-set-parameter" warning.
* For references like `rq` this warning isn't issued.
*/
template
static void ToJSValVector(std::index_sequence, const ScriptRequest& rq,
[[maybe_unused]] JS::MutableHandleValueVector argv, const Types&... params)
{
(Script::ToJSVal(rq, argv[idx], params), ...);
}
/**
* Wrapper around calling a JS function from C++.
* Arguments are const& to avoid lvalue/rvalue issues, and so can't be used as out-parameters.
* In particular, the problem is that Rooted are deduced as Rooted, not Handle, and so can't be copied.
* This could be worked around with more templates, but it doesn't seem particularly worth doing.
*/
template
static bool Call_(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args)
{
JS::RootedObject obj(rq.cx);
if (!JS_ValueToObject(rq.cx, val, &obj) || !obj)
return false;
// Fetch the property explicitly - this avoids converting the arguments if it doesn't exist.
JS::RootedValue func(rq.cx);
if (!JS_GetProperty(rq.cx, obj, name, &func) || func.isUndefined())
return false;
JS::RootedValueVector argv(rq.cx);
ignore_result(argv.resize(sizeof...(Args)));
ToJSValVector(std::index_sequence_for{}, rq, &argv, args...);
bool success;
if constexpr (std::is_same_v)
success = JS_CallFunctionValue(rq.cx, obj, func, argv, ret);
else
{
JS::RootedValue jsRet(rq.cx);
success = JS_CallFunctionValue(rq.cx, obj, func, argv, &jsRet);
if constexpr (!std::is_same_v)
{
if (success)
Script::FromJSVal(rq, jsRet, ret);
}
else
UNUSED2(ret); // VS2017 complains.
}
// Even if everything succeeded, there could be pending exceptions
return !ScriptException::CatchPending(rq) && success;
}
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
public:
template
using ObjectGetter = T*(*)(const ScriptRequest&, JS::CallArgs&);
// TODO: the fact that this takes class and not auto is to work around an odd VS17 bug.
// It can be removed with VS19.
template
using GetterFor = ObjectGetter::object_type>;
/**
* The meat of this file. This wraps a C++ function into a JSNative,
* so that it can be called from JS and manipulated in Spidermonkey.
* Most C++ functions can be directly wrapped, so long as their arguments are
* convertible from JS::Value and their return value is convertible to JS::Value (or void)
* The C++ function may optionally take const ScriptRequest& or ScriptInterface& as its first argument.
* The function may be an object method, in which case you need to pass an appropriate getter
*
* Optimisation note: the ScriptRequest object is created even without arguments,
* as it's necessary for IsExceptionPending.
*
* @param thisGetter to get the object, if necessary.
*/
template thisGetter = nullptr>
static bool ToJSNative(JSContext* cx, unsigned argc, JS::Value* vp)
{
using ObjType = typename args_info::object_type;
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
ScriptRequest rq(cx);
// If the callable is an object method, we must specify how to fetch the object.
static_assert(std::is_same_v::object_type, void> || thisGetter != nullptr,
"ScriptFunction::Register - No getter specified for object method");
// GCC 7 triggers spurious warnings
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Waddress"
#endif
ObjType* obj = nullptr;
if constexpr (thisGetter != nullptr)
{
obj = thisGetter(rq, args);
if (!obj)
return false;
}
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
bool wentOk = true;
typename args_info::arg_types outs = ConvertFromJS(rq, args, wentOk,
static_cast::arg_types*>(nullptr));
if (!wentOk)
return false;
/**
* TODO: error handling isn't standard, and since this can call any C++ function,
* there's no simple obvious way to deal with it.
* For now we check for pending JS exceptions, but it would probably be nicer
* to standardise on something, or perhaps provide an "errorHandler" here.
*/
if constexpr (std::is_same_v::return_type>)
call(obj, outs);
else if constexpr (std::is_same_v::return_type>)
args.rval().set(call(obj, outs));
else
Script::ToJSVal(rq, args.rval(), call(obj, outs));
return !ScriptException::IsPending(rq);
}
/**
* Call a JS function @a name, property of object @a val, with the arguments @a args.
* @a ret will be updated with the return value, if any.
* @return the success (or failure) thereof.
*/
template
static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args)
{
return Call_(rq, val, name, ret, std::forward(args)...);
}
// Specialisation for MutableHandleValue return.
template
static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, JS::MutableHandleValue ret, const Args&... args)
{
return Call_(rq, val, name, ret, std::forward(args)...);
}
/**
* Call a JS function @a name, property of object @a val, with the arguments @a args.
* @return the success (or failure) thereof.
*/
template
static bool CallVoid(const ScriptRequest& rq, JS::HandleValue val, const char* name, const Args&... args)
{
return Call(rq, val, name, IgnoreResult, std::forward(args)...);
}
/**
* Return a function spec from a C++ function.
*/
- template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
- static JSFunctionSpec Wrap(const char* name)
+ template thisGetter = nullptr>
+ static JSFunctionSpec Wrap(const char* name,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)
{
return JS_FN(name, (&ToJSNative), args_info::nb_args, flags);
}
/**
* Return a JSFunction from a C++ function.
*/
- template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
- static JSFunction* Create(const ScriptRequest& rq, const char* name)
+ template thisGetter = nullptr>
+ static JSFunction* Create(const ScriptRequest& rq, const char* name,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)
{
return JS_NewFunction(rq.cx, &ToJSNative, args_info::nb_args, flags, name);
}
/**
* Register a function on the native scope (usually 'Engine').
*/
- template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
- static void Register(const ScriptRequest& rq, const char* name)
+ template thisGetter = nullptr>
+ static void Register(const ScriptRequest& rq, const char* name,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)
{
JS_DefineFunction(rq.cx, rq.nativeScope, name, &ToJSNative, args_info::nb_args, flags);
}
/**
* Register a function on @param scope.
* Prefer the version taking ScriptRequest unless you have a good reason not to.
* @see Register
*/
- template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT>
- static void Register(JSContext* cx, JS::HandleObject scope, const char* name)
+ template thisGetter = nullptr>
+ static void Register(JSContext* cx, JS::HandleObject scope, const char* name,
+ const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)
{
JS_DefineFunction(cx, scope, name, &ToJSNative, args_info::nb_args, flags);
}
};
#endif // INCLUDED_FUNCTIONWRAPPER