Index: ps/trunk/source/graphics/MapGenerator.h =================================================================== --- ps/trunk/source/graphics/MapGenerator.h +++ ps/trunk/source/graphics/MapGenerator.h @@ -18,223 +18,27 @@ #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/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp +++ ps/trunk/source/graphics/MapGenerator.cpp @@ -29,8 +29,8 @@ #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" @@ -39,16 +39,16 @@ #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()) @@ -60,362 +60,369 @@ 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/MapReader.h =================================================================== --- ps/trunk/source/graphics/MapReader.h +++ ps/trunk/source/graphics/MapReader.h @@ -26,6 +26,8 @@ #include "scriptinterface/ScriptTypes.h" #include "simulation2/system/Entity.h" +#include + class CTerrain; class WaterManager; class SkyManager; @@ -38,7 +40,6 @@ class CTerrainTextureEntry; class CGameView; class CXMLReader; -class CMapGenerator; class ScriptContext; class ScriptInterface; @@ -87,7 +88,8 @@ int LoadRMSettings(); // Generate random map - int GenerateMap(const CStrW& scriptFile); + int StartMapGeneration(const CStrW& scriptFile); + int PollMapGeneration(); // Parse script data into terrain int ParseTerrain(); @@ -103,7 +105,7 @@ // 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 @@ -119,7 +121,8 @@ JS::PersistentRootedValue m_ScriptSettings; JS::PersistentRootedValue m_MapData; - CMapGenerator* m_MapGen; + struct GeneratorState; + std::unique_ptr m_GeneratorState; CFileUnpacker unpacker; CTerrain* pTerrain; @@ -141,10 +144,11 @@ 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}; }; /** Index: ps/trunk/source/graphics/MapReader.cpp =================================================================== --- ps/trunk/source/graphics/MapReader.cpp +++ ps/trunk/source/graphics/MapReader.cpp @@ -32,6 +32,7 @@ #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" @@ -39,6 +40,7 @@ #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" @@ -61,11 +63,10 @@ #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_, @@ -218,8 +219,13 @@ // 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) @@ -1320,56 +1326,70 @@ 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; }; @@ -1652,5 +1672,4 @@ { // Cleaup objects delete xml_reader; - delete m_MapGen; } Index: ps/trunk/source/graphics/tests/test_MapGenerator.h =================================================================== --- ps/trunk/source/graphics/tests/test_MapGenerator.h +++ ps/trunk/source/graphics/tests/test_MapGenerator.h @@ -1,4 +1,4 @@ -/* 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 @@ -19,6 +19,8 @@ #include "ps/Filesystem.h" #include "simulation2/system/ComponentTest.h" +#include + class TestMapGenerator : public CxxTest::TestSuite { public: @@ -52,9 +54,16 @@ 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.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VFS.h +++ ps/trunk/source/ps/scripting/JSInterface_VFS.h @@ -1,4 +1,4 @@ -/* 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 @@ -22,9 +22,12 @@ 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/ps/scripting/JSInterface_VFS.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VFS.cpp +++ ps/trunk/source/ps/scripting/JSInterface_VFS.cpp @@ -286,30 +286,33 @@ 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/scriptinterface/FunctionWrapper.h =================================================================== --- ps/trunk/source/scriptinterface/FunctionWrapper.h +++ ps/trunk/source/scriptinterface/FunctionWrapper.h @@ -349,8 +349,9 @@ /** * 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); } @@ -358,8 +359,9 @@ /** * 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); } @@ -367,8 +369,9 @@ /** * 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); } @@ -378,8 +381,9 @@ * 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); }