Index: source/graphics/MapGenerator.h =================================================================== --- source/graphics/MapGenerator.h +++ source/graphics/MapGenerator.h @@ -19,222 +19,40 @@ #define INCLUDED_MAPGENERATOR #include "ps/FileIo.h" -#include "ps/Future.h" -#include "ps/TemplateLoader.h" #include "scriptinterface/StructuredClone.h" #include -#include -#include -#include #include -class CMapGeneratorWorker; +class TestMapGenerator; -/** - * 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 +class MapGenerator { - 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; - + friend TestMapGenerator; /** - * Get random map data, according to this format: - * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat - * - * @return StructuredClone containing map data + * This function can only be called from the test. */ - Script::StructuredClone GetResults(); + static Script::StructuredClone GenerateMap(std::atomic& progress, + ScriptInterface& scriptInterface, const VfsPath& script, const std::string& settings, + const bool isTest); -private: - CMapGeneratorWorker* m_Worker; - -}; - -/** - * Random map generator worker thread. - * (This is run in a thread so that the GUI remains responsive while loading) - * - * 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. - */ -class CMapGeneratorWorker -{ public: - CMapGeneratorWorker(ScriptInterface* scriptInterface); - ~CMapGeneratorWorker(); /** - * Start the map generator thread + * Generate the map. This will take long. * - * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" + * @param progress Is updated by this function. See Loader.h for the semantics. + * @param scriptInterface The scriptInterface to use. + * @param script 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; + * @return random map data, according to this format: + * https://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat + */ + static Script::StructuredClone GenerateMap(std::atomic& progress, + ScriptInterface& scriptInterface, const VfsPath& script, const std::string& settings) + { + return GenerateMap(progress, scriptInterface, script, settings, false); + } }; - #endif //INCLUDED_MAPGENERATOR Index: source/graphics/MapGenerator.cpp =================================================================== --- source/graphics/MapGenerator.cpp +++ source/graphics/MapGenerator.cpp @@ -30,8 +30,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/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" @@ -39,12 +39,11 @@ #include "scriptinterface/JSON.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 @@ -60,136 +59,310 @@ return true; } -CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) : - m_ScriptInterface(scriptInterface) -{} - -CMapGeneratorWorker::~CMapGeneratorWorker() +/** + * Provides callback's for the JavaScript. + */ +struct CMapGeneratorCallbackData { - // Cancel or wait for the task to end. - m_WorkerThread.CancelOrWait(); -} + CMapGeneratorCallbackData(ScriptInterface& scriptInterface, std::atomic& progress, + const boost::rand48::result_type seed) : + m_ScriptInterface{scriptInterface}, + m_MapGenRNG{seed}, + m_Progress{progress} + { + m_ScriptInterface.SetCallbackData(static_cast(this)); -void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) -{ - std::lock_guard lock(m_WorkerMutex); + // Enable the script to be aborted + JS_AddInterruptCallback(m_ScriptInterface.GetGeneralJSContext(), &MapGeneratorInterruptCallback); + } - // Set progress to positive value - m_Progress.store(1); - m_ScriptPath = scriptFile; - m_Settings = settings; + ~CMapGeneratorCallbackData() + { + JS_AddInterruptCallback(m_ScriptInterface.GetGeneralJSContext(), nullptr); + m_ScriptInterface.SetCallbackData(nullptr); + } - // Start generating the map asynchronously. - m_WorkerThread = Threading::TaskManager::Instance().PushTask([this]() { - PROFILE2("Map Generation"); + /** + * 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) + { + // Ignore libraries that are already loaded + if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) + return true; - std::shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); + // Mark this as loaded, to prevent it recursively loading itself + m_LoadedLibraries.insert(libraryName); - // Enable the script to be aborted - JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback); + VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); + VfsPaths pathnames; - m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); + // 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()); - // Run map generation scripts - if (!Run() || m_Progress.load() > 0) + if (!m_ScriptInterface.LoadGlobalScriptFile(p)) + { + LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); + return false; + } + } + } + else { - // Don't leave progress in an unknown state, if generator failed, set it to -1 - m_Progress.store(-1); + // 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; } - SAFE_DELETE(m_ScriptInterface); + return true; + } - // 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. - }); -} + /** + * Finalize map generation and pass results from the script to the engine. + */ + void ExportMap(JS::HandleValue data) + { + // Copy results + m_MapData = Script::WriteStructuredClone(ScriptRequest(m_ScriptInterface), data); + m_Progress.store(0); + } -bool CMapGeneratorWorker::Run() -{ - ScriptRequest rq(m_ScriptInterface); + /** + * 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(); + } - // Parse settings - JS::RootedValue settingsVal(rq.cx); - if (!Script::ParseJSON(rq, m_Settings, &settingsVal) && settingsVal.isUndefined()) + 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) { - LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); - return false; + ScriptRequest rq(m_ScriptInterface); + + if (!VfsFileExists(filename)) + { + ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str()); + return JS::UndefinedValue(); + } + + CFileUnpacker unpacker; + unpacker.Read(filename, "PSMP"); + + 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(); + } + + // 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) + { + CStr texturename; + unpacker.UnpackString(texturename); + textureNames.push_back(texturename); + } + + // 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) + { + 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; } - // Prevent unintentional modifications to the settings object by random map scripts - if (!Script::FreezeObject(rq, settingsVal, true)) + /** + * Sets the map generation progress, which is one of multiple stages determining the loading screen progress. + */ + void SetProgress(int progress) { - LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); - 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); } - // 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"); + /** + * Microseconds since the epoch. + */ + double GetMicroseconds() + { + return JS_Now(); + } + + /** + * 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()); - InitScriptInterface(seed); + return templateRoot; + } - RegisterScriptFunctions_MapGenerator(); + /** + * Check whether the given template exists. + */ + bool TemplateExists(const std::string& templateName) + { + return m_TemplateLoader.TemplateExists(templateName); + } - // Copy settings to global variable - JS::RootedValue global(rq.cx, rq.globalValue()); - if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true)) + /** + * Returns all template names of simulation entity templates. + */ + std::vector FindTemplates(const std::string& path, bool includeSubdirectories) { - LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); - return false; + return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } - // Load RMS - LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); - if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) + /** + * Returns all template names of actors. + */ + std::vector FindActorTemplates(const std::string& path, bool includeSubdirectories) { - LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); - return false; + return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } - return true; -} + /** + * 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; + + /** + * Provides the script context. + */ + ScriptInterface& m_ScriptInterface; + + /** + * Backend to loading template data. + */ + CTemplateLoader m_TemplateLoader; +}; + #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); + ScriptFunction::Register<&CMapGeneratorCallbackData::func, ScriptInterface::ObjectFromCBData>(rq, #func); -void CMapGeneratorWorker::InitScriptInterface(const u32 seed) +/** + * Set initial seed, callback data. + * Expose functions, globals and classes defined in this class relevant to the map and test scripts. + */ +static void InitScriptInterface(CMapGeneratorCallbackData& callbackData) { - m_ScriptInterface->SetCallbackData(static_cast(this)); - - m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); - m_MapGenRNG.seed(seed); + callbackData.m_ScriptInterface.ReplaceNondeterministicRNG(callbackData.m_MapGenRNG); // VFS - JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(*m_ScriptInterface); + JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(callbackData.m_ScriptInterface); // Globalscripts may use VFS script functions - m_ScriptInterface->LoadGlobalScripts(); + callbackData.m_ScriptInterface.LoadGlobalScripts(); // File loading - ScriptRequest rq(m_ScriptInterface); - REGISTER_MAPGEN_FUNC_NAME(LoadScripts, "LoadLibrary"); - REGISTER_MAPGEN_FUNC_NAME(LoadHeightmap, "LoadHeightmapImage"); + ScriptRequest rq(callbackData.m_ScriptInterface); + REGISTER_MAPGEN_FUNC(LoadLibrary); + REGISTER_MAPGEN_FUNC(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)); + callbackData.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)); + callbackData.m_ScriptInterface.SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); } -void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator() +/** + * Expose functions defined in this class that are relevant to mapscripts but not the tests. + */ +static void RegisterScriptFunctions_MapGenerator(CMapGeneratorCallbackData& callbackData) { - ScriptRequest rq(m_ScriptInterface); + ScriptRequest rq(callbackData.m_ScriptInterface); // Template functions REGISTER_MAPGEN_FUNC(GetTemplate); @@ -204,218 +377,61 @@ } #undef REGISTER_MAPGEN_FUNC -#undef REGISTER_MAPGEN_FUNC_NAME -int CMapGeneratorWorker::GetProgress() const -{ - return m_Progress.load(); -} -double CMapGeneratorWorker::GetMicroseconds() +Script::StructuredClone MapGenerator::GenerateMap(std::atomic& progress, ScriptInterface& scriptInterface, + const VfsPath& script, const std::string& settings, const bool isTest) { - return JS_Now(); -} + ScriptRequest rq(scriptInterface); -Script::StructuredClone CMapGeneratorWorker::GetResults() -{ - std::lock_guard lock(m_WorkerMutex); - return m_MapData; -} - -void CMapGeneratorWorker::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); -} - -std::vector CMapGeneratorWorker::FindActorTemplates(const std::string& path, bool includeSubdirectories) -{ - return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); -} - -bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) -{ - // Ignore libraries that are already loaded - if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) - return true; - - // Mark this as loaded, to prevent it recursively loading itself - m_LoadedLibraries.insert(libraryName); - - VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); - VfsPaths pathnames; - - // 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()); - - if (!m_ScriptInterface->LoadGlobalScriptFile(p)) - { - LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); - return false; - } - } - } - else - { - // 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; - } - - return true; -} - -JS::Value CMapGeneratorWorker::LoadHeightmap(const VfsPath& filename) -{ - std::vector heightmap; - if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) - { - LOGERROR("Could not load heightmap file '%s'", filename.string8()); - return JS::UndefinedValue(); - } - - ScriptRequest rq(m_ScriptInterface); - JS::RootedValue returnValue(rq.cx); - Script::ToJSVal(rq, &returnValue, heightmap); - return returnValue; -} - -// See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering -JS::Value CMapGeneratorWorker::LoadMapTerrain(const VfsPath& filename) -{ - ScriptRequest rq(m_ScriptInterface); - - if (!VfsFileExists(filename)) + // Parse settings + JS::RootedValue settingsVal(rq.cx); + if (!Script::ParseJSON(rq, settings, &settingsVal) && settingsVal.isUndefined()) { - ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str()); - return JS::UndefinedValue(); + LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); + progress.store(-1); + return nullptr; } - CFileUnpacker unpacker; - unpacker.Read(filename, "PSMP"); - - if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) + // Prevent unintentional modifications to the settings object by random map scripts + if (!Script::FreezeObject(rq, settingsVal, true)) { - ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", filename.string8().c_str()); - return JS::UndefinedValue(); + LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); + progress.store(-1); + return nullptr; } - // unpack size - ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); - size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; + // 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"); - // unpack heightmap - std::vector heightmap; - heightmap.resize(SQR(verticesPerSide)); - unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); + CMapGeneratorCallbackData callbackData{scriptInterface, progress, seed}; + InitScriptInterface(callbackData); - // unpack texture names - size_t textureCount = unpacker.UnpackSize(); - std::vector textureNames; - textureNames.reserve(textureCount); - for (size_t i = 0; i < textureCount; ++i) + if (!isTest) { - CStr texturename; - unpacker.UnpackString(texturename); - textureNames.push_back(texturename); - } - - // 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()); + RegisterScriptFunctions_MapGenerator(callbackData); - // 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) + // Copy settings to global variable + JS::RootedValue global(rq.cx, rq.globalValue()); + if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true)) { - 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); + LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); + progress.store(-1); + return nullptr; } } - JS::RootedValue returnValue(rq.cx); - - Script::CreateObject( - rq, - &returnValue, - "height", heightmap, - "textureNames", textureNames, - "textureIDs", textureIDs); - - return returnValue; -} - -////////////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////// - -CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr)) -{ -} - -CMapGenerator::~CMapGenerator() -{ - delete m_Worker; -} - -void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) -{ - m_Worker->Initialize(scriptFile, settings); -} - -int CMapGenerator::GetProgress() const -{ - return m_Worker->GetProgress(); -} + // Load RMS + LOGMESSAGE("Loading RMS '%s'", script.string8()); + if (!scriptInterface.LoadGlobalScriptFile(script)) + { + LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", script.string8()); + progress.store(-1); + return nullptr; + } -Script::StructuredClone CMapGenerator::GetResults() -{ - return m_Worker->GetResults(); + return progress.load() == 0 ? std::move(callbackData.m_MapData) : nullptr; } Index: source/graphics/MapReader.h =================================================================== --- source/graphics/MapReader.h +++ source/graphics/MapReader.h @@ -38,9 +38,9 @@ class CTerrainTextureEntry; class CGameView; class CXMLReader; -class CMapGenerator; class ScriptContext; class ScriptInterface; +struct GeneratorState; class CMapReader : public CMapIO { @@ -123,7 +123,7 @@ JS::PersistentRootedValue m_ScriptSettings; JS::PersistentRootedValue m_MapData; - CMapGenerator* m_MapGen; + std::unique_ptr m_Generator; CFileUnpacker unpacker; CTerrain* pTerrain; Index: source/graphics/MapReader.cpp =================================================================== --- source/graphics/MapReader.cpp +++ source/graphics/MapReader.cpp @@ -33,6 +33,7 @@ #include "ps/CLogger.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" +#include "ps/TaskManager.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/PostprocManager.h" @@ -40,6 +41,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" @@ -62,8 +64,11 @@ #pragma warning(disable: 4458) // Declaration hides class member. #endif +// TODO: Maybe this should be optimized depending on the map size. +constexpr int RMS_CONTEXT_SIZE = 96 * 1024 * 1024; + CMapReader::CMapReader() - : xml_reader(0), m_PatchesPerSide(0), m_MapGen(0) + : xml_reader(0), m_PatchesPerSide(0) { cur_terrain_tex = 0; // important - resets generator state } @@ -1271,14 +1276,24 @@ return 0; } +struct GeneratorState +{ + std::atomic progress{-1}; + Future task; + + ~GeneratorState() + { + task.CancelOrWait(); + } +}; int CMapReader::GenerateMap() { ScriptRequest rq(pSimulation2->GetScriptInterface()); - if (!m_MapGen) + if (!m_Generator) { // Initialize map generator - m_MapGen = new CMapGenerator(); + m_Generator = std::make_unique(); VfsPath scriptPath; @@ -1288,12 +1303,23 @@ // Stringify settings to pass across threads std::string scriptSettings = Script::StringifyJSON(rq, &m_ScriptSettings); + // Set a positive value. + m_Generator->progress.store(1); // Try to generate map - m_MapGen->GenerateMap(scriptPath, scriptSettings); + m_Generator->task = Threading::TaskManager::Instance().PushTask([=] + { + PROFILE2("Map Generation"); + + std::shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); + + ScriptInterface scriptInterface{"Engine", "MapGenerator", mapgenContext}; + return MapGenerator::GenerateMap(m_Generator->progress, scriptInterface, scriptPath, + scriptSettings); + }); } // Check status - int progress = m_MapGen->GetProgress(); + int progress = m_Generator->progress.load(); if (progress < 0) { // RMS failed - return to main menu @@ -1302,7 +1328,7 @@ 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(); + Script::StructuredClone results = m_Generator->task.Get(); // Parse data into simulation context JS::RootedValue data(rq.cx); @@ -1603,5 +1629,4 @@ { // Cleaup objects delete xml_reader; - delete m_MapGen; } Index: source/graphics/tests/test_MapGenerator.h =================================================================== --- source/graphics/tests/test_MapGenerator.h +++ source/graphics/tests/test_MapGenerator.h @@ -19,6 +19,8 @@ #include "ps/Filesystem.h" #include "simulation2/system/ComponentTest.h" +#include + class TestMapGenerator : public CxxTest::TestSuite { public: @@ -52,9 +54,8 @@ ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext); ScriptTestSetup(scriptInterface); - CMapGeneratorWorker worker(&scriptInterface); - worker.InitScriptInterface(0); - scriptInterface.LoadGlobalScriptFile(path); + std::atomic progress; + MapGenerator::GenerateMap(progress, scriptInterface, path, "{\"Seed\": 0}", true); } } };