Index: source/graphics/MapGenerator.h =================================================================== --- source/graphics/MapGenerator.h +++ source/graphics/MapGenerator.h @@ -19,222 +19,44 @@ #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 { - NONCOPYABLE(CMapGenerator); + friend TestMapGenerator; 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 + * `progress` will take on this value if an error occurred. */ - void GenerateMap(const VfsPath& scriptFile, const std::string& settings); + static constexpr int INVALID_PROGRESS{-1}; /** - * Get status of the map generator thread + * Generate the map. This does take a long time. * - * @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) - * - * 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 - * - * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" + * @param progress param progress Destination to write the function + * progress to. You must not write to it while GenerateMap is running. + * See Loader.h for the semantics. + * @param script The VFS path for the script, e.g. "maps/random/latium.js" * @param settings JSON string containing settings for the map generator + * @return random map data, according to this format: + * https://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat */ - 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); + static Script::StructuredClone GenerateMap(std::atomic& progress, const VfsPath& script, + const std::string& settings); 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. + * This function is intended for tests and internal use. */ - 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; + static Script::StructuredClone GenerateMap(std::atomic& progress, + ScriptInterface& scriptInterface, const VfsPath& script, const std::string& settings, + const bool isTest); }; - #endif //INCLUDED_MAPGENERATOR Index: source/graphics/MapGenerator.cpp =================================================================== --- source/graphics/MapGenerator.cpp +++ 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,6 +39,8 @@ #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/MapEdgeTiles.h" +#include +#include #include #include @@ -47,8 +49,9 @@ extern bool IsQuitRequested(); -static bool -MapGeneratorInterruptCallback(JSContext* UNUSED(cx)) +namespace +{ +bool MapGeneratorInterruptCallback(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 +63,395 @@ 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 CMapGeneratorCallbackData { - 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"); - - std::shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); +public: + // These functions are called by C++. + + CMapGeneratorCallbackData(ScriptInterface& scriptInterface, std::atomic& progress, + Script::StructuredClone& mapData) : + m_ScriptInterface{scriptInterface}, + m_MapData{mapData}, + m_Progress{progress} + { + m_ScriptInterface.SetCallbackData(static_cast(this)); // Enable the script to be aborted - JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback); - - m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); - - // 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); - } - - SAFE_DELETE(m_ScriptInterface); - - // 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. - }); -} - -bool CMapGeneratorWorker::Run() -{ - ScriptRequest rq(m_ScriptInterface); - - // 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; + JS_AddInterruptCallback(m_ScriptInterface.GetGeneralJSContext(), &MapGeneratorInterruptCallback); } - // Prevent unintentional modifications to the settings object by random map scripts - if (!Script::FreezeObject(rq, settingsVal, true)) + ~CMapGeneratorCallbackData() { - 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); +#define REGISTER_MAPGEN_FUNC(func) \ + ScriptFunction::Register<&CMapGeneratorCallbackData::func, ScriptInterface::ObjectFromCBData>(rq, #func); - RegisterScriptFunctions_MapGenerator(); - // Copy settings to global variable - JS::RootedValue global(rq.cx, rq.globalValue()); - if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true)) + /** + * Set initial seed, callback data. + * Expose functions, globals and classes defined in this class relevant to the map and test scripts. + */ + void InitScriptInterface() { - LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); - return false; - } + // VFS + JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(m_ScriptInterface); - // 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; - } + // Globalscripts may use VFS script functions + m_ScriptInterface.LoadGlobalScripts(); - return true; -} - -#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); + // File loading + ScriptRequest rq(m_ScriptInterface); + REGISTER_MAPGEN_FUNC(LoadLibrary); + REGISTER_MAPGEN_FUNC(LoadHeightmapImage); + REGISTER_MAPGEN_FUNC(LoadMapTerrain); -void CMapGeneratorWorker::InitScriptInterface(const u32 seed) -{ - m_ScriptInterface->SetCallbackData(static_cast(this)); + // Engine constants - m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); - m_MapGenRNG.seed(seed); + // 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)); - // VFS - JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(*m_ScriptInterface); + // Number of impassable tiles at the map border + m_ScriptInterface.SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); + } - // Globalscripts may use VFS script functions - m_ScriptInterface->LoadGlobalScripts(); + /** + * Expose functions defined in this class that are relevant to mapscripts but not the tests. + */ + void 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); + } - // File loading - ScriptRequest rq(m_ScriptInterface); - REGISTER_MAPGEN_FUNC_NAME(LoadScripts, "LoadLibrary"); - REGISTER_MAPGEN_FUNC_NAME(LoadHeightmap, "LoadHeightmapImage"); - REGISTER_MAPGEN_FUNC(LoadMapTerrain); +#undef REGISTER_MAPGEN_FUNC - // 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)); + // These functions are called by JS. - // Number of impassable tiles at the map border - m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); -} + /** + * 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; -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); -} + // Mark this as loaded, to prevent it recursively loading itself + m_LoadedLibraries.insert(libraryName); -#undef REGISTER_MAPGEN_FUNC -#undef REGISTER_MAPGEN_FUNC_NAME + VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); + VfsPaths pathnames; -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("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; + } -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); } - 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()); + /** + * 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(); + } - return templateRoot; -} + ScriptRequest rq(m_ScriptInterface); + JS::RootedValue returnValue(rq.cx); + Script::ToJSVal(rq, &returnValue, heightmap); + return returnValue; + } -bool CMapGeneratorWorker::TemplateExists(const std::string& templateName) -{ - return m_TemplateLoader.TemplateExists(templateName); -} + /** + * 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); -std::vector CMapGeneratorWorker::FindTemplates(const std::string& path, bool includeSubdirectories) -{ - return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); -} + if (!VfsFileExists(filename)) + { + ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str()); + return JS::UndefinedValue(); + } -std::vector CMapGeneratorWorker::FindActorTemplates(const std::string& path, bool includeSubdirectories) -{ - return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); -} + CFileUnpacker unpacker; + unpacker.Read(filename, "PSMP"); -bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) -{ - // Ignore libraries that are already loaded - if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) - return true; + 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(); + } - // Mark this as loaded, to prevent it recursively loading itself - m_LoadedLibraries.insert(libraryName); + // unpack size + ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); + size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; - VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); - VfsPaths pathnames; + // unpack heightmap + std::vector heightmap; + heightmap.resize(SQR(verticesPerSide)); + unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); - // 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 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; +private: + /** + * Provides the script context. + */ + ScriptInterface& m_ScriptInterface; + + /** + * Result of the mapscript generation including terrain, entities and environment settings. + */ + Script::StructuredClone& m_MapData; + + /** + * Currently loaded script librarynames. + */ + std::set m_LoadedLibraries; + + /** + * Current map generation progress. + * Initialized to `CMapGenerator::INVALID_PROGRESS`. If something happens before we start, that's a + * failure. + */ + std::atomic& m_Progress; + + /** + * Backend to loading template data. + */ + CTemplateLoader m_TemplateLoader; +}; +} - // unpack heightmap - std::vector heightmap; - heightmap.resize(SQR(verticesPerSide)); - unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); +Script::StructuredClone CMapGenerator::GenerateMap(std::atomic& progress, + ScriptInterface& scriptInterface, const VfsPath& script, const std::string& settings, + const bool isTest) +{ + ScriptRequest rq(scriptInterface); - // unpack texture names - size_t textureCount = unpacker.UnpackSize(); - std::vector textureNames; - textureNames.reserve(textureCount); - for (size_t i = 0; i < textureCount; ++i) + // Parse settings + JS::RootedValue settingsVal(rq.cx); + if (!Script::ParseJSON(rq, settings, &settingsVal) && settingsVal.isUndefined()) { - CStr texturename; - unpacker.UnpackString(texturename); - textureNames.push_back(texturename); + LOGERROR("CMapGenerator::GenerateMap: Failed to parse settings"); + progress.store(CMapGenerator::INVALID_PROGRESS); + return nullptr; } - // 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) + // Prevent unintentional modifications to the settings object by random map scripts + if (!Script::FreezeObject(rq, settingsVal, true)) { - 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); - } + LOGERROR("CMapGenerator::GenerateMap: Failed to deepfreeze settings"); + progress.store(CMapGenerator::INVALID_PROGRESS); + return nullptr; } - JS::RootedValue returnValue(rq.cx); - - Script::CreateObject( - rq, - &returnValue, - "height", heightmap, - "textureNames", textureNames, - "textureIDs", textureIDs); + // 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"); - return returnValue; -} + boost::rand48 mapGenRNG{seed}; + scriptInterface.ReplaceNondeterministicRNG(mapGenRNG); -////////////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////// + Script::StructuredClone mapData; + CMapGeneratorCallbackData callbackData{scriptInterface, progress, mapData}; + callbackData.InitScriptInterface(); -CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr)) -{ -} + if (!isTest) + { + callbackData.RegisterScriptFunctions_MapGenerator(); -CMapGenerator::~CMapGenerator() -{ - delete m_Worker; -} + // Copy settings to global variable + JS::RootedValue global(rq.cx, rq.globalValue()); + if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true)) + { + LOGERROR("CMapGenerator::GenerateMap: Failed to define g_MapSettings"); + progress.store(INVALID_PROGRESS); + return nullptr; + } + } -void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) -{ - m_Worker->Initialize(scriptFile, settings); -} + // Load RMS + LOGMESSAGE("Loading RMS '%s'", script.string8()); + if (!scriptInterface.LoadGlobalScriptFile(script)) + { + LOGERROR("CMapGenerator::GenerateMap: Failed to load RMS '%s'", script.string8()); + progress.store(CMapGenerator::INVALID_PROGRESS); + return nullptr; + } -int CMapGenerator::GetProgress() const -{ - return m_Worker->GetProgress(); + return progress.load() == 0 ? std::move(mapData) : nullptr; } -Script::StructuredClone CMapGenerator::GetResults() +Script::StructuredClone CMapGenerator::GenerateMap(std::atomic& progress, const VfsPath& script, + const std::string& settings) { - return m_Worker->GetResults(); + const std::shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); + ScriptInterface scriptInterface{"Engine", "MapGenerator", mapgenContext}; + return CMapGenerator::GenerateMap(progress, scriptInterface, script, settings, false); } Index: source/graphics/MapReader.h =================================================================== --- source/graphics/MapReader.h +++ 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; @@ -119,7 +120,8 @@ JS::PersistentRootedValue m_ScriptSettings; JS::PersistentRootedValue m_MapData; - CMapGenerator* m_MapGen; + struct GeneratorState; + std::unique_ptr m_GeneratorState; CFileUnpacker unpacker; CTerrain* pTerrain; Index: source/graphics/MapReader.cpp =================================================================== --- source/graphics/MapReader.cpp +++ 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" @@ -62,7 +64,7 @@ #endif 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 } @@ -1320,30 +1322,50 @@ return 0; } +struct CMapReader::GeneratorState +{ + std::atomic progress{CMapGenerator::INVALID_PROGRESS}; + Future task; + + ~GeneratorState() + { + task.CancelOrWait(); + } +}; + int CMapReader::GenerateMap(const CStrW& scriptFile) { ScriptRequest rq(pSimulation2->GetScriptInterface()); - if (!m_MapGen) + if (!m_GeneratorState) { // Initialize map generator - m_MapGen = new CMapGenerator(); - - VfsPath scriptPath; - - 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); + // No error occured. Set `progress` to a non-error value. + // Don't do it in the task since `progress` might be checked before the task is started. + // Don't do it after the task is started since we must not write to `progress` while + // `GenerateMap` is running. + m_GeneratorState->progress.store(1); + // Try to generate map - m_MapGen->GenerateMap(scriptPath, scriptSettings); + m_GeneratorState->task = Threading::TaskManager::Instance().PushTask( + [this, scriptFile, settings = std::move(scriptSettings)] + { + PROFILE2("Map Generation"); + + const auto scriptPath = scriptFile.empty() ? VfsPath{} : L"maps/random/" + scriptFile; + + return CMapGenerator::GenerateMap(m_GeneratorState->progress, scriptPath, settings); + }); } // Check status - int progress = m_MapGen->GetProgress(); - if (progress < 0) + const int progress = m_GeneratorState->progress.load(); + if (progress == CMapGenerator::INVALID_PROGRESS) { // RMS failed - return to main menu throw PSERROR_Game_World_MapLoadFailed("Error generating random map.\nCheck application log for details."); @@ -1351,7 +1373,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_GeneratorState->task.Get(); // Parse data into simulation context JS::RootedValue data(rq.cx); @@ -1652,5 +1674,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 @@ -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,14 @@ ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext); ScriptTestSetup(scriptInterface); - CMapGeneratorWorker worker(&scriptInterface); - worker.InitScriptInterface(0); - scriptInterface.LoadGlobalScriptFile(path); + // Initialize `progress`. Don't set it to 0 that would mean its complete. Don't set it to + // -1 that would mean an error occured. + std::atomic progress{1}; + + // The test script don't call `ExportMap` so `GenerateMap` allways returns `nullptr`. + TS_ASSERT_EQUALS( + CMapGenerator::GenerateMap(progress, scriptInterface, path, "{\"Seed\": 0}", true), + nullptr); } } };