Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 27943) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 27944) @@ -1,421 +1,428 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MapGenerator.h" #include "graphics/MapIO.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "lib/status.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_path.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/FileIo.h" #include "ps/Profile.h" -#include "ps/TaskManager.h" #include "ps/scripting/JSInterface_VFS.h" +#include "ps/TemplateLoader.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/JSON.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/MapEdgeTiles.h" +#include +#include #include #include -// TODO: Maybe this should be optimized depending on the map size. -constexpr int RMS_CONTEXT_SIZE = 96 * 1024 * 1024; - extern bool IsQuitRequested(); -static bool -MapGeneratorInterruptCallback(JSContext* UNUSED(cx)) +namespace +{ +bool MapGenerationInterruptCallback(JSContext* UNUSED(cx)) { // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents if (IsQuitRequested()) { LOGWARNING("Quit requested!"); return false; } return true; } -CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) : - m_ScriptInterface(scriptInterface) -{} - -CMapGeneratorWorker::~CMapGeneratorWorker() -{ - // Cancel or wait for the task to end. - m_WorkerThread.CancelOrWait(); -} - -void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) +/** + * Provides callback's for the JavaScript. + */ +class CMapGenerationCallbacks { - std::lock_guard lock(m_WorkerMutex); - - // Set progress to positive value - m_Progress.store(1); - m_ScriptPath = scriptFile; - m_Settings = settings; - - // Start generating the map asynchronously. - m_WorkerThread = Threading::TaskManager::Instance().PushTask([this]() { - PROFILE2("Map Generation"); +public: + // Only the constructor and the destructor are called by C++. - std::shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); + CMapGenerationCallbacks(std::atomic& progress, ScriptInterface& scriptInterface, + Script::StructuredClone& mapData, const u16 flags) : + m_Progress{progress}, + m_ScriptInterface{scriptInterface}, + m_MapData{mapData} + { + m_ScriptInterface.SetCallbackData(static_cast(this)); // Enable the script to be aborted - JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback); + JS_AddInterruptCallback(m_ScriptInterface.GetGeneralJSContext(), + &MapGenerationInterruptCallback); - m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); + // Set initial seed, callback data. + // Expose functions, globals and classes relevant to the map scripts. +#define REGISTER_MAPGEN_FUNC(func) \ + ScriptFunction::Register<&CMapGenerationCallbacks::func, \ + ScriptInterface::ObjectFromCBData>(rq, #func, flags); - // Run map generation scripts - if (!Run() || m_Progress.load() > 0) - { - // Don't leave progress in an unknown state, if generator failed, set it to -1 - m_Progress.store(-1); - } + // VFS + JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(m_ScriptInterface, flags); - SAFE_DELETE(m_ScriptInterface); + // Globalscripts may use VFS script functions + m_ScriptInterface.LoadGlobalScripts(); - // At this point the random map scripts are done running, so the thread has no further purpose - // and can die. The data will be stored in m_MapData already if successful, or m_Progress - // will contain an error value on failure. - }); -} + // File loading + ScriptRequest rq(m_ScriptInterface); + REGISTER_MAPGEN_FUNC(LoadLibrary); + REGISTER_MAPGEN_FUNC(LoadHeightmapImage); + REGISTER_MAPGEN_FUNC(LoadMapTerrain); + + // Template functions + REGISTER_MAPGEN_FUNC(GetTemplate); + REGISTER_MAPGEN_FUNC(TemplateExists); + REGISTER_MAPGEN_FUNC(FindTemplates); + REGISTER_MAPGEN_FUNC(FindActorTemplates); + + // Progression and profiling + REGISTER_MAPGEN_FUNC(SetProgress); + REGISTER_MAPGEN_FUNC(GetMicroseconds); + REGISTER_MAPGEN_FUNC(ExportMap); + + // Engine constants + + // Length of one tile of the terrain grid in metres. + // Useful to transform footprint sizes to the tilegrid coordinate system. + m_ScriptInterface.SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE)); -bool CMapGeneratorWorker::Run() -{ - ScriptRequest rq(m_ScriptInterface); + // Number of impassable tiles at the map border + m_ScriptInterface.SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); - // Parse settings - JS::RootedValue settingsVal(rq.cx); - if (!Script::ParseJSON(rq, m_Settings, &settingsVal) && settingsVal.isUndefined()) - { - LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); - return false; +#undef REGISTER_MAPGEN_FUNC } - // Prevent unintentional modifications to the settings object by random map scripts - if (!Script::FreezeObject(rq, settingsVal, true)) + ~CMapGenerationCallbacks() { - LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); - return false; + JS_AddInterruptCallback(m_ScriptInterface.GetGeneralJSContext(), nullptr); + m_ScriptInterface.SetCallbackData(nullptr); } - // Init RNG seed - u32 seed = 0; - if (!Script::HasProperty(rq, settingsVal, "Seed") || - !Script::GetProperty(rq, settingsVal, "Seed", seed)) - LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0"); - - InitScriptInterface(seed); +private: - RegisterScriptFunctions_MapGenerator(); + // These functions are called by JS. - // Copy settings to global variable - JS::RootedValue global(rq.cx, rq.globalValue()); - if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true)) + /** + * Load all scripts of the given library + * + * @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/) + * @return true if all scripts ran successfully, false if there's an error + */ + bool LoadLibrary(const VfsPath& libraryName) { - LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); - return false; - } + // Ignore libraries that are already loaded + if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) + return true; - // Load RMS - LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); - if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) - { - LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); - return false; - } + // Mark this as loaded, to prevent it recursively loading itself + m_LoadedLibraries.insert(libraryName); - return true; -} + VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); + VfsPaths pathnames; -#define REGISTER_MAPGEN_FUNC(func) \ - ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData>(rq, #func); -#define REGISTER_MAPGEN_FUNC_NAME(func, name) \ - ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData>(rq, name); - -void CMapGeneratorWorker::InitScriptInterface(const u32 seed) -{ - m_ScriptInterface->SetCallbackData(static_cast(this)); - - m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); - m_MapGenRNG.seed(seed); - - // VFS - JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(*m_ScriptInterface); - - // Globalscripts may use VFS script functions - m_ScriptInterface->LoadGlobalScripts(); - - // File loading - ScriptRequest rq(m_ScriptInterface); - REGISTER_MAPGEN_FUNC_NAME(LoadScripts, "LoadLibrary"); - REGISTER_MAPGEN_FUNC_NAME(LoadHeightmap, "LoadHeightmapImage"); - REGISTER_MAPGEN_FUNC(LoadMapTerrain); - - // Engine constants - - // Length of one tile of the terrain grid in metres. - // Useful to transform footprint sizes to the tilegrid coordinate system. - m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE)); - - // Number of impassable tiles at the map border - m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); -} - -void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator() -{ - ScriptRequest rq(m_ScriptInterface); - - // Template functions - REGISTER_MAPGEN_FUNC(GetTemplate); - REGISTER_MAPGEN_FUNC(TemplateExists); - REGISTER_MAPGEN_FUNC(FindTemplates); - REGISTER_MAPGEN_FUNC(FindActorTemplates); - - // Progression and profiling - REGISTER_MAPGEN_FUNC(SetProgress); - REGISTER_MAPGEN_FUNC(GetMicroseconds); - REGISTER_MAPGEN_FUNC(ExportMap); -} - -#undef REGISTER_MAPGEN_FUNC -#undef REGISTER_MAPGEN_FUNC_NAME - -int CMapGeneratorWorker::GetProgress() const -{ - return m_Progress.load(); -} + // Load all scripts in mapgen directory + Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); + if (ret == INFO::OK) + { + for (const VfsPath& p : pathnames) + { + LOGMESSAGE("Loading map generator script '%s'", p.string8()); -double CMapGeneratorWorker::GetMicroseconds() -{ - return JS_Now(); -} + if (!m_ScriptInterface.LoadGlobalScriptFile(p)) + { + LOGERROR("CMapGenerationCallbacks::LoadScripts: Failed to load script '%s'", + p.string8()); + return false; + } + } + } + else + { + // Some error reading directory + wchar_t error[200]; + LOGERROR( + "CMapGenerationCallbacks::LoadScripts: Error reading scripts in directory '%s': %s", + path.string8(), + utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); + return false; + } -Script::StructuredClone CMapGeneratorWorker::GetResults() -{ - std::lock_guard lock(m_WorkerMutex); - return m_MapData; -} + return true; + } -void CMapGeneratorWorker::ExportMap(JS::HandleValue data) -{ + /** + * Finalize map generation and pass results from the script to the engine. + * The `data` has to be according to this format: + * https://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat + */ + void ExportMap(JS::HandleValue data) { // Copy results - std::lock_guard lock(m_WorkerMutex); m_MapData = Script::WriteStructuredClone(ScriptRequest(m_ScriptInterface), data); } - m_Progress.store(0); -} -void CMapGeneratorWorker::SetProgress(int progress) -{ - // When the task is started, `m_Progress` is only mutated by this thread. - const int currentProgress = m_Progress.load(); - if (progress >= currentProgress) - m_Progress.store(progress); - else - LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", - currentProgress, progress); -} - -CParamNode CMapGeneratorWorker::GetTemplate(const std::string& templateName) -{ - const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild(); - if (!templateRoot.IsOk()) - LOGERROR("Invalid template found for '%s'", templateName.c_str()); - - return templateRoot; -} - -bool CMapGeneratorWorker::TemplateExists(const std::string& templateName) -{ - return m_TemplateLoader.TemplateExists(templateName); -} - -std::vector CMapGeneratorWorker::FindTemplates(const std::string& path, bool includeSubdirectories) -{ - return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); -} + /** + * Load an image file and return it as a height array. + */ + JS::Value LoadHeightmapImage(const VfsPath& filename) + { + std::vector heightmap; + if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) + { + LOGERROR("Could not load heightmap file '%s'", filename.string8()); + return JS::UndefinedValue(); + } -std::vector CMapGeneratorWorker::FindActorTemplates(const std::string& path, bool includeSubdirectories) -{ - return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); -} + ScriptRequest rq(m_ScriptInterface); + JS::RootedValue returnValue(rq.cx); + Script::ToJSVal(rq, &returnValue, heightmap); + return returnValue; + } + + /** + * Load an Atlas terrain file (PMP) returning textures and heightmap. + * + * See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering + */ + JS::Value LoadMapTerrain(const VfsPath& filename) + { + ScriptRequest rq(m_ScriptInterface); -bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) -{ - // Ignore libraries that are already loaded - if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) - return true; + if (!VfsFileExists(filename)) + { + ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", + filename.string8().c_str()); + return JS::UndefinedValue(); + } - // Mark this as loaded, to prevent it recursively loading itself - m_LoadedLibraries.insert(libraryName); + CFileUnpacker unpacker; + unpacker.Read(filename, "PSMP"); - VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); - VfsPaths pathnames; + if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) + { + ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", + filename.string8().c_str()); + return JS::UndefinedValue(); + } - // Load all scripts in mapgen directory - Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); - if (ret == INFO::OK) - { - for (const VfsPath& p : pathnames) + // unpack size + ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); + size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; + + // unpack heightmap + std::vector heightmap; + heightmap.resize(SQR(verticesPerSide)); + unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); + + // unpack texture names + size_t textureCount = unpacker.UnpackSize(); + std::vector textureNames; + textureNames.reserve(textureCount); + for (size_t i = 0; i < textureCount; ++i) { - LOGMESSAGE("Loading map generator script '%s'", p.string8()); + CStr texturename; + unpacker.UnpackString(texturename); + textureNames.push_back(texturename); + } - if (!m_ScriptInterface->LoadGlobalScriptFile(p)) + // unpack texture IDs per tile + ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE; + std::vector tiles; + tiles.resize(size_t(SQR(tilesPerSide))); + unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size()); + + // reorder by patches and store and save texture IDs per tile + std::vector textureIDs; + for (ssize_t x = 0; x < tilesPerSide; ++x) + { + size_t patchX = x / PATCH_SIZE; + size_t offX = x % PATCH_SIZE; + for (ssize_t y = 0; y < tilesPerSide; ++y) { - LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); - return false; + size_t patchY = y / PATCH_SIZE; + size_t offY = y % PATCH_SIZE; + // m_Priority and m_Tex2Index unused + textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + + (offY * PATCH_SIZE + offX)].m_Tex1Index); } } + + JS::RootedValue returnValue(rq.cx); + + Script::CreateObject( + rq, + &returnValue, + "height", heightmap, + "textureNames", textureNames, + "textureIDs", textureIDs); + + return returnValue; } - else + + /** + * Sets the map generation progress, which is one of multiple stages + * determining the loading screen progress. + */ + void SetProgress(int progress) { - // Some error reading directory - wchar_t error[200]; - LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); - return false; + // When the task is started, `m_Progress` is only mutated by this thread. + const int currentProgress = m_Progress.load(); + if (progress >= currentProgress) + m_Progress.store(progress); + else + LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", + currentProgress, progress); } - return true; -} - -JS::Value CMapGeneratorWorker::LoadHeightmap(const VfsPath& filename) -{ - std::vector heightmap; - if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) + /** + * Microseconds since the epoch. + */ + double GetMicroseconds() const { - LOGERROR("Could not load heightmap file '%s'", filename.string8()); - return JS::UndefinedValue(); + return JS_Now(); } - ScriptRequest rq(m_ScriptInterface); - JS::RootedValue returnValue(rq.cx); - Script::ToJSVal(rq, &returnValue, heightmap); - return returnValue; -} + /** + * Return the template data of the given template name. + */ + CParamNode GetTemplate(const std::string& templateName) + { + const CParamNode& templateRoot = + m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild(); + if (!templateRoot.IsOk()) + LOGERROR("Invalid template found for '%s'", templateName.c_str()); -// See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering -JS::Value CMapGeneratorWorker::LoadMapTerrain(const VfsPath& filename) -{ - ScriptRequest rq(m_ScriptInterface); + return templateRoot; + } - if (!VfsFileExists(filename)) + /** + * Check whether the given template exists. + */ + bool TemplateExists(const std::string& templateName) const { - ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str()); - return JS::UndefinedValue(); + return m_TemplateLoader.TemplateExists(templateName); } - CFileUnpacker unpacker; - unpacker.Read(filename, "PSMP"); + /** + * Returns all template names of simulation entity templates. + */ + std::vector FindTemplates(const std::string& path, bool includeSubdirectories) + { + return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); + } - if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) + /** + * Returns all template names of actors. + */ + std::vector FindActorTemplates(const std::string& path, bool includeSubdirectories) { - ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", filename.string8().c_str()); - return JS::UndefinedValue(); + return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } - // unpack size - ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); - size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; + /** + * Current map generation progress. + */ + std::atomic& m_Progress; - // unpack heightmap - std::vector heightmap; - heightmap.resize(SQR(verticesPerSide)); - unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); + /** + * Provides the script context. + */ + ScriptInterface& m_ScriptInterface; - // unpack texture names - size_t textureCount = unpacker.UnpackSize(); - std::vector textureNames; - textureNames.reserve(textureCount); - for (size_t i = 0; i < textureCount; ++i) - { - CStr texturename; - unpacker.UnpackString(texturename); - textureNames.push_back(texturename); - } + /** + * Result of the mapscript generation including terrain, entities and environment settings. + */ + Script::StructuredClone& m_MapData; - // unpack texture IDs per tile - ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE; - std::vector tiles; - tiles.resize(size_t(SQR(tilesPerSide))); - unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size()); + /** + * Currently loaded script librarynames. + */ + std::set m_LoadedLibraries; - // reorder by patches and store and save texture IDs per tile - std::vector textureIDs; - for (ssize_t x = 0; x < tilesPerSide; ++x) - { - size_t patchX = x / PATCH_SIZE; - size_t offX = x % PATCH_SIZE; - for (ssize_t y = 0; y < tilesPerSide; ++y) - { - size_t patchY = y / PATCH_SIZE; - size_t offY = y % PATCH_SIZE; - // m_Priority and m_Tex2Index unused - textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index); - } - } + /** + * Backend to loading template data. + */ + CTemplateLoader m_TemplateLoader; +}; +} // anonymous namespace - JS::RootedValue returnValue(rq.cx); +Script::StructuredClone RunMapGenerationScript(std::atomic& progress, ScriptInterface& scriptInterface, + const VfsPath& script, const std::string& settings, const u16 flags) +{ + ScriptRequest rq(scriptInterface); - Script::CreateObject( - rq, - &returnValue, - "height", heightmap, - "textureNames", textureNames, - "textureIDs", textureIDs); + // Parse settings + JS::RootedValue settingsVal(rq.cx); + if (!Script::ParseJSON(rq, settings, &settingsVal) && settingsVal.isUndefined()) + { + LOGERROR("RunMapGenerationScript: Failed to parse settings"); + return nullptr; + } - return returnValue; -} + // Prevent unintentional modifications to the settings object by random map scripts + if (!Script::FreezeObject(rq, settingsVal, true)) + { + LOGERROR("RunMapGenerationScript: Failed to deepfreeze settings"); + return nullptr; + } -////////////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////////// + // Init RNG seed + u32 seed = 0; + if (!Script::HasProperty(rq, settingsVal, "Seed") || + !Script::GetProperty(rq, settingsVal, "Seed", seed)) + LOGWARNING("RunMapGenerationScript: No seed value specified - using 0"); -CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr)) -{ -} + boost::rand48 mapGenRNG{seed}; + scriptInterface.ReplaceNondeterministicRNG(mapGenRNG); -CMapGenerator::~CMapGenerator() -{ - delete m_Worker; -} + Script::StructuredClone mapData; + CMapGenerationCallbacks callbackData{progress, scriptInterface, mapData, flags}; -void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) -{ - m_Worker->Initialize(scriptFile, settings); -} + // Copy settings to global variable + JS::RootedValue global(rq.cx, rq.globalValue()); + if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, flags & JSPROP_READONLY, + flags & JSPROP_ENUMERATE)) + { + LOGERROR("RunMapGenerationScript: Failed to define g_MapSettings"); + return nullptr; + } -int CMapGenerator::GetProgress() const -{ - return m_Worker->GetProgress(); -} + // Load RMS + LOGMESSAGE("Loading RMS '%s'", script.string8()); + if (!scriptInterface.LoadGlobalScriptFile(script)) + { + LOGERROR("RunMapGenerationScript: Failed to load RMS '%s'", script.string8()); + return nullptr; + } -Script::StructuredClone CMapGenerator::GetResults() -{ - return m_Worker->GetResults(); + return mapData; } Index: ps/trunk/source/graphics/MapGenerator.h =================================================================== --- ps/trunk/source/graphics/MapGenerator.h (revision 27943) +++ ps/trunk/source/graphics/MapGenerator.h (revision 27944) @@ -1,240 +1,44 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_MAPGENERATOR #define INCLUDED_MAPGENERATOR -#include "ps/FileIo.h" -#include "ps/Future.h" -#include "ps/TemplateLoader.h" +#include "lib/file/vfs/vfs_path.h" #include "scriptinterface/StructuredClone.h" #include -#include -#include -#include #include -class CMapGeneratorWorker; - -/** - * Random map generator interface. Initialized by CMapReader and then checked - * periodically during loading, until it's finished (progress value is 0). - * - * The actual work is performed by CMapGeneratorWorker in a separate thread. - */ -class CMapGenerator -{ - NONCOPYABLE(CMapGenerator); - -public: - CMapGenerator(); - ~CMapGenerator(); - - /** - * Start the map generator thread - * - * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" - * @param settings JSON string containing settings for the map generator - */ - void GenerateMap(const VfsPath& scriptFile, const std::string& settings); - - /** - * Get status of the map generator thread - * - * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error - */ - int GetProgress() const; - - /** - * Get random map data, according to this format: - * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat - * - * @return StructuredClone containing map data - */ - Script::StructuredClone GetResults(); - -private: - CMapGeneratorWorker* m_Worker; - -}; - /** - * Random map generator worker thread. - * (This is run in a thread so that the GUI remains responsive while loading) + * Generate the map. This does take a long time. * - * Thread-safety: - * - Initialize and constructor/destructor must be called from the main thread. - * - ScriptInterface created and destroyed by thread - * - StructuredClone used to return JS map data - JS:Values can't be used across threads/contexts. + * @param progress Destination to write the function progress to. You must not + * write to it while `RunMapGenerationScript` is running. + * @param script The VFS path for the script, e.g. "maps/random/latium.js". + * @param settings JSON string containing settings for the map generator. + * @param flags With thous flags the engine functions get registered + * `g_MapSettings` also respects this flags. + * @return If there is an error `nullptr` is returned. Otherwise random map + * data, according to this format: + * https://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat */ -class CMapGeneratorWorker -{ -public: - CMapGeneratorWorker(ScriptInterface* scriptInterface); - ~CMapGeneratorWorker(); - - /** - * Start the map generator thread - * - * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" - * @param settings JSON string containing settings for the map generator - */ - void Initialize(const VfsPath& scriptFile, const std::string& settings); - - /** - * Get status of the map generator thread - * - * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error - */ - int GetProgress() const; - - /** - * Get random map data, according to this format: - * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat - * - * @return StructuredClone containing map data - */ - Script::StructuredClone GetResults(); - - /** - * Set initial seed, callback data. - * Expose functions, globals and classes defined in this class relevant to the map and test scripts. - */ - void InitScriptInterface(const u32 seed); - -private: - - /** - * Expose functions defined in this class that are relevant to mapscripts but not the tests. - */ - void RegisterScriptFunctions_MapGenerator(); - - /** - * Load all scripts of the given library - * - * @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/) - * @return true if all scripts ran successfully, false if there's an error - */ - bool LoadScripts(const VfsPath& libraryName); - - /** - * Finalize map generation and pass results from the script to the engine. - */ - void ExportMap(JS::HandleValue data); - - /** - * Load an image file and return it as a height array. - */ - JS::Value LoadHeightmap(const VfsPath& src); - - /** - * Load an Atlas terrain file (PMP) returning textures and heightmap. - */ - JS::Value LoadMapTerrain(const VfsPath& filename); - - /** - * Sets the map generation progress, which is one of multiple stages determining the loading screen progress. - */ - void SetProgress(int progress); - - /** - * Microseconds since the epoch. - */ - double GetMicroseconds(); - - /** - * Return the template data of the given template name. - */ - CParamNode GetTemplate(const std::string& templateName); - - /** - * Check whether the given template exists. - */ - bool TemplateExists(const std::string& templateName); - - /** - * Returns all template names of simulation entity templates. - */ - std::vector FindTemplates(const std::string& path, bool includeSubdirectories); - - /** - * Returns all template names of actors. - */ - std::vector FindActorTemplates(const std::string& path, bool includeSubdirectories); - - /** - * Perform the map generation. - */ - bool Run(); - - /** - * Currently loaded script librarynames. - */ - std::set m_LoadedLibraries; - - /** - * Result of the mapscript generation including terrain, entities and environment settings. - */ - Script::StructuredClone m_MapData; - - /** - * Deterministic random number generator. - */ - boost::rand48 m_MapGenRNG; - - /** - * Current map generation progress. - * Initialize to `-1`. If something happens before we start, that's a - * failure. - */ - std::atomic m_Progress{-1}; - - /** - * Provides the script context. - */ - ScriptInterface* m_ScriptInterface; - - /** - * Map generation script to run. - */ - VfsPath m_ScriptPath; - - /** - * Map and simulation settings chosen in the gamesetup stage. - */ - std::string m_Settings; - - /** - * Backend to loading template data. - */ - CTemplateLoader m_TemplateLoader; - - /** - * Holds the completion result of the asynchronous map generation. - * TODO: this whole class could really be a future on its own. - */ - Future m_WorkerThread; - - /** - * Avoids thread synchronization issues. - */ - std::mutex m_WorkerMutex; -}; - +Script::StructuredClone RunMapGenerationScript(std::atomic& progress, + ScriptInterface& scriptInterface, const VfsPath& script, const std::string& settings, + const u16 flags = JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); #endif //INCLUDED_MAPGENERATOR Index: ps/trunk/source/graphics/MapReader.cpp =================================================================== --- ps/trunk/source/graphics/MapReader.cpp (revision 27943) +++ ps/trunk/source/graphics/MapReader.cpp (revision 27944) @@ -1,1656 +1,1675 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MapReader.h" #include "graphics/Camera.h" #include "graphics/CinemaManager.h" #include "graphics/Entity.h" #include "graphics/GameView.h" #include "graphics/MapGenerator.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Loader.h" +#include "ps/TaskManager.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/PostprocManager.h" #include "renderer/SkyManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptContext.h" +#include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRequest.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpCinemaManager.h" #include "simulation2/components/ICmpGarrisonHolder.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpTurretHolder.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpWaterManager.h" #include #if defined(_MSC_VER) && _MSC_VER > 1900 #pragma warning(disable: 4456) // Declaration hides previous local declaration. #pragma warning(disable: 4458) // Declaration hides class member. #endif -CMapReader::CMapReader() - : xml_reader(0), m_PatchesPerSide(0), m_MapGen(0) -{ - cur_terrain_tex = 0; // important - resets generator state -} +// TODO: Maybe this should be optimized depending on the map size. +constexpr int MAP_GENERATION_CONTEXT_SIZE{96 * MiB}; + +CMapReader::CMapReader() = default; // LoadMap: try to load the map from given file; reinitialise the scene to new data if successful void CMapReader::LoadMap(const VfsPath& pathname, const ScriptContext& cx, JS::HandleValue settings, CTerrain *pTerrain_, WaterManager* pWaterMan_, SkyManager* pSkyMan_, CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_, CSimulation2 *pSimulation2_, const CSimContext* pSimContext_, int playerID_, bool skipEntities) { pTerrain = pTerrain_; pLightEnv = pLightEnv_; pGameView = pGameView_; pWaterMan = pWaterMan_; pSkyMan = pSkyMan_; pCinema = pCinema_; pTrigMan = pTrigMan_; pPostproc = pPostproc_; pSimulation2 = pSimulation2_; pSimContext = pSimContext_; m_PlayerID = playerID_; m_SkipEntities = skipEntities; m_StartingCameraTarget = INVALID_ENTITY; m_ScriptSettings.init(cx.GetGeneralJSContext(), settings); filename_xml = pathname.ChangeExtension(L".xml"); // In some cases (particularly tests) we don't want to bother storing a large // mostly-empty .pmp file, so we let the XML file specify basic terrain instead. // If there's an .xml file and no .pmp, then we're probably in this XML-only mode only_xml = false; if (!VfsFileExists(pathname) && VfsFileExists(filename_xml)) { only_xml = true; } file_format_version = CMapIO::FILE_VERSION; // default if there's no .pmp if (!only_xml) { // [25ms] unpacker.Read(pathname, "PSMP"); file_format_version = unpacker.GetVersion(); } // check oldest supported version if (file_format_version < FILE_READ_VERSION) throw PSERROR_Game_World_MapLoadFailed("Could not load terrain file - too old version!"); // delete all existing entities if (pSimulation2) pSimulation2->ResetState(); // reset post effects if (pPostproc) pPostproc->SetPostEffect(L"default"); // load map or script settings script if (settings.isUndefined()) LDR_Register([this](const double) { return LoadScriptSettings(); }, L"CMapReader::LoadScriptSettings", 50); else LDR_Register([this](const double) { return LoadRMSettings(); }, L"CMapReader::LoadRMSettings", 50); // load player settings script (must be done before reading map) LDR_Register([this](const double) { return LoadPlayerSettings(); }, L"CMapReader::LoadPlayerSettings", 50); // unpack the data if (!only_xml) LDR_Register([this](const double) { return UnpackTerrain(); }, L"CMapReader::UnpackMap", 1200); // read the corresponding XML file LDR_Register([this](const double) { return ReadXML(); }, L"CMapReader::ReadXML", 50); // apply terrain data to the world LDR_Register([this](const double) { return ApplyTerrainData(); }, L"CMapReader::ApplyTerrainData", 5); // read entities LDR_Register([this](const double) { return ReadXMLEntities(); }, L"CMapReader::ReadXMLEntities", 5800); // apply misc data to the world LDR_Register([this](const double) { return ApplyData(); }, L"CMapReader::ApplyData", 5); // load map settings script (must be done after reading map) LDR_Register([this](const double) { return LoadMapSettings(); }, L"CMapReader::LoadMapSettings", 5); } // LoadRandomMap: try to load the map data; reinitialise the scene to new data if successful void CMapReader::LoadRandomMap(const CStrW& scriptFile, const ScriptContext& cx, JS::HandleValue settings, CTerrain *pTerrain_, WaterManager* pWaterMan_, SkyManager* pSkyMan_, CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_, CSimulation2 *pSimulation2_, int playerID_) { pSimulation2 = pSimulation2_; pSimContext = pSimulation2 ? &pSimulation2->GetSimContext() : NULL; m_ScriptSettings.init(cx.GetGeneralJSContext(), settings); pTerrain = pTerrain_; pLightEnv = pLightEnv_; pGameView = pGameView_; pWaterMan = pWaterMan_; pSkyMan = pSkyMan_; pCinema = pCinema_; pTrigMan = pTrigMan_; pPostproc = pPostproc_; m_PlayerID = playerID_; m_SkipEntities = false; m_StartingCameraTarget = INVALID_ENTITY; // delete all existing entities if (pSimulation2) pSimulation2->ResetState(); only_xml = false; // copy random map settings (before entity creation) LDR_Register([this](const double) { return LoadRMSettings(); }, L"CMapReader::LoadRMSettings", 50); // load player settings script (must be done before reading map) LDR_Register([this](const double) { return LoadPlayerSettings(); }, L"CMapReader::LoadPlayerSettings", 50); // load map generator with random map script LDR_Register([this, scriptFile](const double) { - return GenerateMap(scriptFile); - }, L"CMapReader::GenerateMap", 20000); + return StartMapGeneration(scriptFile); + }, L"CMapReader::StartMapGeneration", 1); + + LDR_Register([this](const double) + { + return PollMapGeneration(); + }, L"CMapReader::PollMapGeneration", 19999); // parse RMS results into terrain structure LDR_Register([this](const double) { return ParseTerrain(); }, L"CMapReader::ParseTerrain", 500); // parse RMS results into environment settings LDR_Register([this](const double) { return ParseEnvironment(); }, L"CMapReader::ParseEnvironment", 5); // parse RMS results into camera settings LDR_Register([this](const double) { return ParseCamera(); }, L"CMapReader::ParseCamera", 5); // apply terrain data to the world LDR_Register([this](const double) { return ApplyTerrainData(); }, L"CMapReader::ApplyTerrainData", 5); // parse RMS results into entities LDR_Register([this](const double) { return ParseEntities(); }, L"CMapReader::ParseEntities", 1000); // apply misc data to the world LDR_Register([this](const double) { return ApplyData(); }, L"CMapReader::ApplyData", 5); // load map settings script (must be done after reading map) LDR_Register([this](const double) { return LoadMapSettings(); }, L"CMapReader::LoadMapSettings", 5); } // UnpackTerrain: unpack the terrain from the end of the input data stream // - data: map size, heightmap, list of textures used by map, texture tile assignments int CMapReader::UnpackTerrain() { // yield after this time is reached. balances increased progress bar // smoothness vs. slowing down loading. const double end_time = timer_Time() + 200e-3; // first call to generator (this is skipped after first call, // i.e. when the loop below was interrupted) if (cur_terrain_tex == 0) { m_PatchesPerSide = (ssize_t)unpacker.UnpackSize(); // unpack heightmap [600us] size_t verticesPerSide = m_PatchesPerSide*PATCH_SIZE+1; m_Heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16)); // unpack # textures num_terrain_tex = unpacker.UnpackSize(); m_TerrainTextures.reserve(num_terrain_tex); } // unpack texture names; find handle for each texture. // interruptible. while (cur_terrain_tex < num_terrain_tex) { CStr texturename; unpacker.UnpackString(texturename); if (CTerrainTextureManager::IsInitialised()) { CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename); m_TerrainTextures.push_back(texentry); } cur_terrain_tex++; LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex); } // unpack tile data [3ms] ssize_t tilesPerSide = m_PatchesPerSide*PATCH_SIZE; m_Tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&m_Tiles[0], sizeof(STileDesc)*m_Tiles.size()); // reset generator state. cur_terrain_tex = 0; return 0; } int CMapReader::ApplyTerrainData() { if (m_PatchesPerSide == 0) { // we'll probably crash when trying to use this map later throw PSERROR_Game_World_MapLoadFailed("Error loading map: no terrain data.\nCheck application log for details."); } if (!only_xml) { // initialise the terrain pTerrain->Initialize(m_PatchesPerSide, &m_Heightmap[0]); if (CTerrainTextureManager::IsInitialised()) { // setup the textures on the minipatches STileDesc* tileptr = &m_Tiles[0]; for (ssize_t j=0; jGetPatch(i,j)->m_MiniPatches[m][k]; // can't fail mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index]; mp.Priority = tileptr->m_Priority; tileptr++; } } } } } } CmpPtr cmpTerrain(*pSimContext, SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); return 0; } // ApplyData: take all the input data, and rebuild the scene from it int CMapReader::ApplyData() { // copy over the lighting parameters if (pLightEnv) *pLightEnv = m_LightEnv; CmpPtr cmpPlayerManager(*pSimContext, SYSTEM_ENTITY); if (pGameView && cmpPlayerManager) { // Default to global camera (with constraints) pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus()); // TODO: Starting rotation? CmpPtr cmpPlayer(*pSimContext, cmpPlayerManager->GetPlayerByID(m_PlayerID)); if (cmpPlayer && cmpPlayer->HasStartingCamera()) { // Use player starting camera CFixedVector3D pos = cmpPlayer->GetStartingCameraPos(); pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat())); } else if (m_StartingCameraTarget != INVALID_ENTITY) { // Point camera at entity CmpPtr cmpPosition(*pSimContext, m_StartingCameraTarget); if (cmpPosition) { CFixedVector3D pos = cmpPosition->GetPosition(); pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat())); } } } return 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// PSRETURN CMapSummaryReader::LoadMap(const VfsPath& pathname) { VfsPath filename_xml = pathname.ChangeExtension(L".xml"); CXeromyces xmb_file; if (xmb_file.Load(g_VFS, filename_xml, "scenario") != PSRETURN_OK) return PSRETURN_File_ReadFailed; // Define all the relevant elements used in the XML file #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(scenario); EL(scriptsettings); #undef AT #undef EL XMBElement root = xmb_file.GetRoot(); ENSURE(root.GetNodeName() == el_scenario); XERO_ITER_EL(root, child) { int child_name = child.GetNodeName(); if (child_name == el_scriptsettings) { m_ScriptSettings = child.GetText(); } } return PSRETURN_OK; } void CMapSummaryReader::GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { ScriptRequest rq(scriptInterface); Script::CreateObject(rq, ret); if (m_ScriptSettings.empty()) return; JS::RootedValue scriptSettingsVal(rq.cx); Script::ParseJSON(rq, m_ScriptSettings, &scriptSettingsVal); Script::SetProperty(rq, ret, "settings", scriptSettingsVal, false); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Holds various state data while reading maps, so that loading can be // interrupted (e.g. to update the progress display) then later resumed. class CXMLReader { NONCOPYABLE(CXMLReader); public: CXMLReader(const VfsPath& xml_filename, CMapReader& mapReader) : m_MapReader(mapReader), nodes(NULL, 0, NULL) { Init(xml_filename); } CStr ReadScriptSettings(); // read everything except for entities void ReadXML(); // return semantics: see Loader.cpp!LoadFunc. int ProgressiveReadEntities(); private: CXeromyces xmb_file; CMapReader& m_MapReader; int el_entity; int el_tracks; int el_template, el_player; int el_position, el_orientation, el_obstruction; int el_garrison; int el_turrets; int el_actor; int at_x; int at_y; int at_z; int at_group, at_group2; int at_angle; int at_uid; int at_seed; int at_turret; XMBElementList nodes; // children of root // loop counters size_t node_idx; size_t entity_idx; // # entities+nonentities processed and total (for progress calc) int completed_jobs, total_jobs; // maximum used entity ID, so we can safely allocate new ones entity_id_t max_uid; void Init(const VfsPath& xml_filename); void ReadTerrain(XMBElement parent); void ReadEnvironment(XMBElement parent); void ReadCamera(XMBElement parent); void ReadPaths(XMBElement parent); void ReadTriggers(XMBElement parent); int ReadEntities(XMBElement parent, double end_time); }; void CXMLReader::Init(const VfsPath& xml_filename) { // must only assign once, so do it here node_idx = entity_idx = 0; if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK) throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!"); // define the elements and attributes that are frequently used in the XML file, // so we don't need to do lots of string construction and comparison when // reading the data. // (Needs to be synchronised with the list in CXMLReader - ugh) #define EL(x) el_##x = xmb_file.GetElementID(#x) #define AT(x) at_##x = xmb_file.GetAttributeID(#x) EL(entity); EL(tracks); EL(template); EL(player); EL(position); EL(garrison); EL(turrets); EL(orientation); EL(obstruction); EL(actor); AT(x); AT(y); AT(z); AT(group); AT(group2); AT(angle); AT(uid); AT(seed); AT(turret); #undef AT #undef EL XMBElement root = xmb_file.GetRoot(); ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario"); nodes = root.GetChildNodes(); // find out total number of entities+nonentities // (used when calculating progress) completed_jobs = 0; total_jobs = 0; for (XMBElement node : nodes) total_jobs += node.GetChildNodes().size(); // Find the maximum entity ID, so we can safely allocate new IDs without conflicts max_uid = SYSTEM_ENTITY; XMBElement ents = nodes.GetFirstNamedItem(xmb_file.GetElementID("Entities")); XERO_ITER_EL(ents, ent) { CStr uid = ent.GetAttributes().GetNamedItem(at_uid); max_uid = std::max(max_uid, (entity_id_t)uid.ToUInt()); } } CStr CXMLReader::ReadScriptSettings() { XMBElement root = xmb_file.GetRoot(); ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario"); nodes = root.GetChildNodes(); XMBElement settings = nodes.GetFirstNamedItem(xmb_file.GetElementID("ScriptSettings")); return settings.GetText(); } void CXMLReader::ReadTerrain(XMBElement parent) { #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) AT(patches); AT(texture); AT(priority); AT(height); #undef AT ssize_t patches = 9; CStr texture = "grass1_spring"; int priority = 0; u16 height = 16384; XERO_ITER_ATTR(parent, attr) { if (attr.Name == at_patches) patches = attr.Value.ToInt(); else if (attr.Name == at_texture) texture = attr.Value; else if (attr.Name == at_priority) priority = attr.Value.ToInt(); else if (attr.Name == at_height) height = (u16)attr.Value.ToInt(); } m_MapReader.m_PatchesPerSide = patches; // Load the texture CTerrainTextureEntry* texentry = nullptr; if (CTerrainTextureManager::IsInitialised()) texentry = g_TexMan.FindTexture(texture); m_MapReader.pTerrain->Initialize(patches, NULL); // Fill the heightmap u16* heightmap = m_MapReader.pTerrain->GetHeightMap(); ssize_t verticesPerSide = m_MapReader.pTerrain->GetVerticesPerSide(); for (ssize_t i = 0; i < SQR(verticesPerSide); ++i) heightmap[i] = height; // Fill the texture map for (ssize_t pz = 0; pz < patches; ++pz) { for (ssize_t px = 0; px < patches; ++px) { CPatch* patch = m_MapReader.pTerrain->GetPatch(px, pz); // can't fail for (ssize_t z = 0; z < PATCH_SIZE; ++z) { for (ssize_t x = 0; x < PATCH_SIZE; ++x) { patch->m_MiniPatches[z][x].Tex = texentry; patch->m_MiniPatches[z][x].Priority = priority; } } } } } void CXMLReader::ReadEnvironment(XMBElement parent) { #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(posteffect); EL(skyset); EL(suncolor); EL(sunelevation); EL(sunrotation); EL(ambientcolor); EL(water); EL(waterbody); EL(type); EL(color); EL(tint); EL(height); EL(waviness); EL(murkiness); EL(windangle); EL(fog); EL(fogcolor); EL(fogfactor); EL(fogthickness); EL(postproc); EL(brightness); EL(contrast); EL(saturation); EL(bloom); AT(r); AT(g); AT(b); #undef AT #undef EL XERO_ITER_EL(parent, element) { int element_name = element.GetNodeName(); XMBAttributeList attrs = element.GetAttributes(); if (element_name == el_skyset) { if (m_MapReader.pSkyMan) m_MapReader.pSkyMan->SetSkySet(element.GetText().FromUTF8()); } else if (element_name == el_suncolor) { m_MapReader.m_LightEnv.m_SunColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_sunelevation) { m_MapReader.m_LightEnv.m_Elevation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_sunrotation) { m_MapReader.m_LightEnv.m_Rotation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_ambientcolor) { m_MapReader.m_LightEnv.m_AmbientColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_fog) { XERO_ITER_EL(element, fog) { int fog_element_name = fog.GetNodeName(); if (fog_element_name == el_fogcolor) { XMBAttributeList fogAttributes = fog.GetAttributes(); m_MapReader.m_LightEnv.m_FogColor = RGBColor( fogAttributes.GetNamedItem(at_r).ToFloat(), fogAttributes.GetNamedItem(at_g).ToFloat(), fogAttributes.GetNamedItem(at_b).ToFloat()); } else if (fog_element_name == el_fogfactor) { m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat(); } else if (fog_element_name == el_fogthickness) { m_MapReader.m_LightEnv.m_FogMax = fog.GetText().ToFloat(); } } } else if (element_name == el_postproc) { XERO_ITER_EL(element, postproc) { int post_element_name = postproc.GetNodeName(); if (post_element_name == el_brightness) { m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat(); } else if (post_element_name == el_contrast) { m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat(); } else if (post_element_name == el_saturation) { m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat(); } else if (post_element_name == el_bloom) { m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat(); } else if (post_element_name == el_posteffect) { if (m_MapReader.pPostproc) m_MapReader.pPostproc->SetPostEffect(postproc.GetText().FromUTF8()); } } } else if (element_name == el_water) { XERO_ITER_EL(element, waterbody) { ENSURE(waterbody.GetNodeName() == el_waterbody); XERO_ITER_EL(waterbody, waterelement) { int water_element_name = waterelement.GetNodeName(); if (water_element_name == el_height) { CmpPtr cmpWaterManager(*m_MapReader.pSimContext, SYSTEM_ENTITY); ENSURE(cmpWaterManager); cmpWaterManager->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText())); continue; } // The rest are purely graphical effects, and should be ignored if // graphics are disabled if (!m_MapReader.pWaterMan) continue; if (water_element_name == el_type) { if (waterelement.GetText() == "default") m_MapReader.pWaterMan->m_WaterType = L"ocean"; else m_MapReader.pWaterMan->m_WaterType = waterelement.GetText().FromUTF8(); } #define READ_COLOR(el, out) \ else if (water_element_name == el) \ { \ XMBAttributeList colorAttrs = waterelement.GetAttributes(); \ out = CColor( \ colorAttrs.GetNamedItem(at_r).ToFloat(), \ colorAttrs.GetNamedItem(at_g).ToFloat(), \ colorAttrs.GetNamedItem(at_b).ToFloat(), \ 1.f); \ } #define READ_FLOAT(el, out) \ else if (water_element_name == el) \ { \ out = waterelement.GetText().ToFloat(); \ } \ READ_COLOR(el_color, m_MapReader.pWaterMan->m_WaterColor) READ_COLOR(el_tint, m_MapReader.pWaterMan->m_WaterTint) READ_FLOAT(el_waviness, m_MapReader.pWaterMan->m_Waviness) READ_FLOAT(el_murkiness, m_MapReader.pWaterMan->m_Murkiness) READ_FLOAT(el_windangle, m_MapReader.pWaterMan->m_WindAngle) #undef READ_FLOAT #undef READ_COLOR else debug_warn(L"Invalid map XML data"); } } } else debug_warn(L"Invalid map XML data"); } m_MapReader.m_LightEnv.CalculateSunDirection(); } void CXMLReader::ReadCamera(XMBElement parent) { // defaults if we don't find player starting camera #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(declination); EL(rotation); EL(position); AT(angle); AT(x); AT(y); AT(z); #undef AT #undef EL float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f); CVector3D translation = CVector3D(100, 150, -100); XERO_ITER_EL(parent, element) { int element_name = element.GetNodeName(); XMBAttributeList attrs = element.GetAttributes(); if (element_name == el_declination) { declination = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_rotation) { rotation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_position) { translation = CVector3D( attrs.GetNamedItem(at_x).ToFloat(), attrs.GetNamedItem(at_y).ToFloat(), attrs.GetNamedItem(at_z).ToFloat()); } else debug_warn(L"Invalid map XML data"); } if (m_MapReader.pGameView) { m_MapReader.pGameView->GetCamera()->m_Orientation.SetXRotation(declination); m_MapReader.pGameView->GetCamera()->m_Orientation.RotateY(rotation); m_MapReader.pGameView->GetCamera()->m_Orientation.Translate(translation); m_MapReader.pGameView->GetCamera()->UpdateFrustum(); } } void CXMLReader::ReadPaths(XMBElement parent) { #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(path); EL(rotation); EL(node); EL(position); EL(target); AT(name); AT(timescale); AT(orientation); AT(mode); AT(style); AT(x); AT(y); AT(z); AT(deltatime); #undef EL #undef AT CmpPtr cmpCinemaManager(*m_MapReader.pSimContext, SYSTEM_ENTITY); XERO_ITER_EL(parent, element) { int elementName = element.GetNodeName(); if (elementName == el_path) { CCinemaData pathData; XMBAttributeList attrs = element.GetAttributes(); CStrW pathName(attrs.GetNamedItem(at_name).FromUTF8()); pathData.m_Name = pathName; pathData.m_Timescale = fixed::FromString(attrs.GetNamedItem(at_timescale)); pathData.m_Orientation = attrs.GetNamedItem(at_orientation).FromUTF8(); pathData.m_Mode = attrs.GetNamedItem(at_mode).FromUTF8(); pathData.m_Style = attrs.GetNamedItem(at_style).FromUTF8(); TNSpline positionSpline, targetSpline; fixed lastPositionTime = fixed::Zero(); fixed lastTargetTime = fixed::Zero(); XERO_ITER_EL(element, pathChild) { elementName = pathChild.GetNodeName(); attrs = pathChild.GetAttributes(); // Load node data used for spline if (elementName == el_node) { lastPositionTime += fixed::FromString(attrs.GetNamedItem(at_deltatime)); lastTargetTime += fixed::FromString(attrs.GetNamedItem(at_deltatime)); XERO_ITER_EL(pathChild, nodeChild) { elementName = nodeChild.GetNodeName(); attrs = nodeChild.GetAttributes(); if (elementName == el_position) { CFixedVector3D position(fixed::FromString(attrs.GetNamedItem(at_x)), fixed::FromString(attrs.GetNamedItem(at_y)), fixed::FromString(attrs.GetNamedItem(at_z))); positionSpline.AddNode(position, CFixedVector3D(), lastPositionTime); lastPositionTime = fixed::Zero(); } else if (elementName == el_rotation) { // TODO: Implement rotation slerp/spline as another object } else if (elementName == el_target) { CFixedVector3D targetPosition(fixed::FromString(attrs.GetNamedItem(at_x)), fixed::FromString(attrs.GetNamedItem(at_y)), fixed::FromString(attrs.GetNamedItem(at_z))); targetSpline.AddNode(targetPosition, CFixedVector3D(), lastTargetTime); lastTargetTime = fixed::Zero(); } else LOGWARNING("Invalid cinematic element for node child"); } } else LOGWARNING("Invalid cinematic element for path child"); } // Construct cinema path with data gathered CCinemaPath path(pathData, positionSpline, targetSpline); if (path.Empty()) { LOGWARNING("Path with name '%s' is empty", pathName.ToUTF8()); return; } if (!cmpCinemaManager) continue; if (!cmpCinemaManager->HasPath(pathName)) cmpCinemaManager->AddPath(path); else LOGWARNING("Path with name '%s' already exists", pathName.ToUTF8()); } else LOGWARNING("Invalid path child with name '%s'", element.GetText()); } } void CXMLReader::ReadTriggers(XMBElement UNUSED(parent)) { } int CXMLReader::ReadEntities(XMBElement parent, double end_time) { XMBElementList entities = parent.GetChildNodes(); ENSURE(m_MapReader.pSimulation2); CSimulation2& sim = *m_MapReader.pSimulation2; CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY); while (entity_idx < entities.size()) { // all new state at this scope and below doesn't need to be // wrapped, since we only yield after a complete iteration. XMBElement entity = entities[entity_idx++]; ENSURE(entity.GetNodeName() == el_entity); XMBAttributeList attrs = entity.GetAttributes(); CStr uid = attrs.GetNamedItem(at_uid); ENSURE(!uid.empty()); int EntityUid = uid.ToInt(); CStrW TemplateName; int PlayerID = 0; std::vector Garrison; std::vector> Turrets; CFixedVector3D Position; CFixedVector3D Orientation; long Seed = -1; // Obstruction control groups. entity_id_t ControlGroup = INVALID_ENTITY; entity_id_t ControlGroup2 = INVALID_ENTITY; XERO_ITER_EL(entity, setting) { int element_name = setting.GetNodeName(); //