From b402e5e84242d56b86329d6d5dbdd4bb32c3c9f0 Mon Sep 17 00:00:00 2001 From: Diject Date: Wed, 24 Dec 2025 11:41:42 +0300 Subject: [PATCH 01/23] Use VHGT height data for global map color calculation --- apps/openmw/mwrender/globalmap.cpp | 63 ++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 9606730e7f..20128ff837 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -154,30 +154,59 @@ namespace MWRender { const ESM::Land* land = mLandStore.search(x, y); - for (int cellY = 0; cellY < mCellSize; ++cellY) + if (land != nullptr && (land->mDataTypes & ESM::Land::DATA_VHGT)) { - for (int cellX = 0; cellX < mCellSize; ++cellX) + land->loadData(ESM::Land::DATA_VHGT); + const ESM::Land::LandData* data = land->getLandData(ESM::Land::DATA_VHGT); + + if (data) { - int vertexX = (cellX * 9) / mCellSize; // 0..8 - int vertexY = (cellY * 9) / mCellSize; // 0..8 + const int vhgtSize = 65; - int texelX = (x - mMinX) * mCellSize + cellX; - int texelY = (y - mMinY) * mCellSize + cellY; + for (int cellY = 0; cellY < mCellSize; ++cellY) + { + for (int cellX = 0; cellX < mCellSize; ++cellX) + { + int vx = (cellX * (vhgtSize - 1)) / mCellSize; + int vy = (cellY * (vhgtSize - 1)) / mCellSize; - int lutIndex = 0; - // Converting [-128; 127] WNAM range to [0; 255] index - if (land != nullptr && (land->mDataTypes & ESM::Land::DATA_WNAM)) - lutIndex = static_cast(land->mWnam[vertexY * 9 + vertexX]) + 128; + float height = data->mHeights[vy * vhgtSize + vx] / 128.0f; - // Use getColor to handle all pixel format conversions automatically - osg::Vec4 color = mColorLut->getColor(lutIndex, 0); + // Convert height to LUT index using the same method as WNAM generation + // Normalize height: positive heights divided by 128, negative by 16 + float normalizedHeight = height / (height > 0.0f ? 128.0f : 16.0f); + // Clamp to [-1, 1] range and convert to [0, 255] index + int lutIndex = static_cast(std::clamp(normalizedHeight, -1.0f, 1.0f) * 127.0f) + 128; - // Use setColor to write to output images - image->setColor(color, texelX, texelY); + int texelX = (x - mMinX) * mCellSize + cellX; + int texelY = (y - mMinY) * mCellSize + cellY; - // Set alpha based on lutIndex threshold - osg::Vec4 alpha(0.0f, 0.0f, 0.0f, lutIndex < 128 ? 0.0f : 1.0f); - alphaImage->setColor(alpha, texelX, texelY); + osg::Vec4 color = mColorLut->getColor(lutIndex, 0); + image->setColor(color, texelX, texelY); + + osg::Vec4 alpha(0.0f, 0.0f, 0.0f, lutIndex < 128 ? 0.0f : 1.0f); + alphaImage->setColor(alpha, texelX, texelY); + } + } + } + } + else + { + for (int cellY = 0; cellY < mCellSize; ++cellY) + { + for (int cellX = 0; cellX < mCellSize; ++cellX) + { + int texelX = (x - mMinX) * mCellSize + cellX; + int texelY = (y - mMinY) * mCellSize + cellY; + + int lutIndex = 0; + osg::Vec4 color = mColorLut->getColor(lutIndex, 0); + image->setColor(color, texelX, texelY); + + // Set alpha based on lutIndex threshold + osg::Vec4 alpha(0.0f, 0.0f, 0.0f, lutIndex < 128 ? 0.0f : 1.0f); + alphaImage->setColor(alpha, texelX, texelY); + } } } } From 09aa9484e68cf218dc97cbcb814f677f19f0521a Mon Sep 17 00:00:00 2001 From: Diject Date: Wed, 24 Dec 2025 11:44:11 +0300 Subject: [PATCH 02/23] Add map extraction options to engine and CLI (not implemented yet) --- apps/openmw/engine.hpp | 10 ++++++++++ apps/openmw/options.cpp | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 97b6a78ee9..11be9eac62 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -183,6 +183,10 @@ namespace OMW Translation::Storage mTranslationDataStorage; bool mNewGame; + std::string mWorldMapOutput; + std::string mLocalMapOutput; + bool mExtractMaps; + Files::ConfigurationManager& mCfgMgr; int mGlMaxTextureImageUnits; @@ -264,6 +268,12 @@ namespace OMW void setRandomSeed(unsigned int seed); + void setWorldMapOutput(const std::string& path); + + void setLocalMapOutput(const std::string& path); + + void setExtractMaps(bool extract); + void setRecastMaxLogLevel(Debug::Level value) { mMaxRecastLogLevel = value; } }; } diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index a908e4a488..df78a682c9 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -95,6 +95,15 @@ namespace OpenMW addOption("random-seed", bpo::value()->default_value(Misc::Rng::generateDefaultSeed()), "seed value for random number generator"); + addOption("world-map-output", bpo::value()->default_value(""), + "directory to save world map texture (default: textures/advanced_world_map/custom)"); + + addOption("local-map-output", bpo::value()->default_value(""), + "directory to save local map textures (default: textures/advanced_world_map/local)"); + + addOption("extract-maps", bpo::value()->implicit_value(true)->default_value(false), + "extract world and local map textures and exit"); + return desc; } } From c613c5decc1f3aac867b06560300d9d2c075346d Mon Sep 17 00:00:00 2001 From: Diject Date: Wed, 24 Dec 2025 15:09:27 +0300 Subject: [PATCH 03/23] Add world map extraction feature to OpenMW --- apps/openmw/CMakeLists.txt | 1 + apps/openmw/engine.cpp | 37 ++++++- apps/openmw/main.cpp | 15 +++ apps/openmw/mapextractor.cpp | 192 +++++++++++++++++++++++++++++++++++ apps/openmw/mapextractor.hpp | 54 ++++++++++ 5 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 apps/openmw/mapextractor.cpp create mode 100644 apps/openmw/mapextractor.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 8570d9df60..bc1723282d 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -1,6 +1,7 @@ set(OPENMW_SOURCES engine.cpp options.cpp + mapextractor.cpp ) set(OPENMW_RESOURCES diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 66bd16ea3e..8bd0aad8e1 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -83,6 +83,7 @@ #include "mwstate/statemanagerimp.hpp" +#include "mapextractor.hpp" #include "profile.hpp" namespace @@ -374,6 +375,7 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mExportFonts(false) , mRandomSeed(0) , mNewGame(false) + , mExtractMaps(false) , mCfgMgr(configurationManager) , mGlMaxTextureImageUnits(0) { @@ -958,11 +960,25 @@ void OMW::Engine::go() prepareEngine(); -#ifdef _WIN32 + if (mExtractMaps) + { + Log(Debug::Info) << "Starting map extraction mode..."; + + mStateManager->newGame(true); + + MapExtractor extractor(*mWorld, mWorldMapOutput, mLocalMapOutput); + extractor.extractWorldMap(); + extractor.extractLocalMaps(); + + Log(Debug::Info) << "Map extraction complete. Exiting..."; + return; + } + + #ifdef _WIN32 const auto* statsFile = _wgetenv(L"OPENMW_OSG_STATS_FILE"); -#else + #else const auto* statsFile = std::getenv("OPENMW_OSG_STATS_FILE"); -#endif + #endif std::filesystem::path path; if (statsFile != nullptr) @@ -1128,3 +1144,18 @@ void OMW::Engine::setRandomSeed(unsigned int seed) { mRandomSeed = seed; } + +void OMW::Engine::setWorldMapOutput(const std::string& path) +{ + mWorldMapOutput = path; +} + +void OMW::Engine::setLocalMapOutput(const std::string& path) +{ + mLocalMapOutput = path; +} + +void OMW::Engine::setExtractMaps(bool extract) +{ + mExtractMaps = extract; +} diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 70e48e0cfc..9badb4f31a 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -158,6 +159,20 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati engine.enableFontExport(variables["export-fonts"].as()); engine.setRandomSeed(variables["random-seed"].as()); + std::string worldMapOutput = variables["world-map-output"].as(); + std::string localMapOutput = variables["local-map-output"].as(); + bool extractMaps = variables["extract-maps"].as(); + + if (worldMapOutput.empty() && extractMaps) + worldMapOutput = Files::pathToUnicodeString(std::filesystem::current_path() / "textures" / "advanced_world_map" / "custom"); + + if (localMapOutput.empty() && extractMaps) + localMapOutput = Files::pathToUnicodeString(std::filesystem::current_path() / "textures" / "advanced_world_map" / "local"); + + engine.setWorldMapOutput(worldMapOutput); + engine.setLocalMapOutput(localMapOutput); + engine.setExtractMaps(extractMaps); + return true; } diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp new file mode 100644 index 0000000000..bfe36ee3ef --- /dev/null +++ b/apps/openmw/mapextractor.cpp @@ -0,0 +1,192 @@ +#include "mapextractor.hpp" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "mwbase/environment.hpp" +#include "mwbase/world.hpp" +#include "mwrender/globalmap.hpp" +#include "mwrender/localmap.hpp" +#include "mwrender/renderingmanager.hpp" +#include "mwworld/cellstore.hpp" +#include "mwworld/esmstore.hpp" +#include "mwworld/worldimp.hpp" + +namespace OMW +{ + MapExtractor::MapExtractor( + MWWorld::World& world, const std::string& worldMapOutput, const std::string& localMapOutput) + : mWorld(world) + , mWorldMapOutputDir(worldMapOutput) + , mLocalMapOutputDir(localMapOutput) + { + std::filesystem::create_directories(mWorldMapOutputDir); + std::filesystem::create_directories(mLocalMapOutputDir); + + // Create GlobalMap and LocalMap instances + MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); + if (renderingManager) + { + osg::Group* root = renderingManager->getLightRoot()->getParent(0)->asGroup(); + SceneUtil::WorkQueue* workQueue = renderingManager->getWorkQueue(); + + mGlobalMap = std::make_unique(root, workQueue); + mLocalMap = std::make_unique(root); + } + } + + MapExtractor::~MapExtractor() = default; + + void MapExtractor::extractWorldMap() + { + Log(Debug::Info) << "Extracting world map..."; + + if (!mGlobalMap) + { + Log(Debug::Error) << "Global map not initialized"; + return; + } + + // Temporarily set cell size to 32 pixels for extraction + const int originalCellSize = Settings::map().mGlobalMapCellSize; + Settings::map().mGlobalMapCellSize.set(32); + + mGlobalMap->render(); + mGlobalMap->ensureLoaded(); + + saveWorldMapTexture(); + saveWorldMapInfo(); + + // Restore original cell size + Settings::map().mGlobalMapCellSize.set(originalCellSize); + + Log(Debug::Info) << "World map extraction complete"; + } + + void MapExtractor::saveWorldMapTexture() + { + osg::ref_ptr baseTexture = mGlobalMap->getBaseTexture(); + if (!baseTexture || !baseTexture->getImage()) + { + Log(Debug::Error) << "Failed to get world map base texture"; + return; + } + + osg::Image* image = baseTexture->getImage(); + std::filesystem::path outputPath = mWorldMapOutputDir / "map.png"; + + if (!osgDB::writeImageFile(*image, outputPath.string())) + { + Log(Debug::Error) << "Failed to write world map texture to " << outputPath; + return; + } + + Log(Debug::Info) << "Saved world map texture: " << outputPath; + } + + void MapExtractor::saveWorldMapInfo() + { + int width = mGlobalMap->getWidth(); + int height = mGlobalMap->getHeight(); + + const MWWorld::ESMStore& store = mWorld.getStore(); + int minX = std::numeric_limits::max(); + int maxX = std::numeric_limits::min(); + int minY = std::numeric_limits::max(); + int maxY = std::numeric_limits::min(); + + MWWorld::Store::iterator it = store.get().extBegin(); + for (; it != store.get().extEnd(); ++it) + { + if (it->getGridX() < minX) + minX = it->getGridX(); + if (it->getGridX() > maxX) + maxX = it->getGridX(); + if (it->getGridY() < minY) + minY = it->getGridY(); + if (it->getGridY() > maxY) + maxY = it->getGridY(); + } + + std::filesystem::path infoPath = mWorldMapOutputDir / "mapInfo.yaml"; + std::ofstream file(infoPath); + + if (!file) + { + Log(Debug::Error) << "Failed to create world map info file: " << infoPath; + return; + } + + file << "width: " << width << "\n"; + file << "height: " << height << "\n"; + file << "pixelsPerCell: 32\n"; + file << "gridX:\n"; + file << " min: " << minX << "\n"; + file << " max: " << maxX << "\n"; + file << "gridY:\n"; + file << " min: " << minY << "\n"; + file << " max: " << maxY << "\n"; + file << "file: \"map.png\"\n"; + + file.close(); + + Log(Debug::Info) << "Saved world map info: " << infoPath; + } + + void MapExtractor::extractLocalMaps() + { + Log(Debug::Info) << "Extracting local maps..."; + + saveLocalMapTextures(); + + Log(Debug::Info) << "Local map extraction complete"; + } + + void MapExtractor::saveLocalMapTextures() + { + if (!mLocalMap) + { + Log(Debug::Error) << "Local map not initialized"; + return; + } + + const MWWorld::ESMStore& store = mWorld.getStore(); + int count = 0; + + MWWorld::Store::iterator it = store.get().extBegin(); + for (; it != store.get().extEnd(); ++it) + { + int x = it->getGridX(); + int y = it->getGridY(); + + osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); + if (!texture || !texture->getImage()) + continue; + + std::ostringstream filename; + filename << "(" << x << "," << y << ").png"; + std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); + + if (osgDB::writeImageFile(*texture->getImage(), outputPath.string())) + { + count++; + if (count % 100 == 0) + Log(Debug::Info) << "Saved " << count << " local map textures..."; + } + } + + Log(Debug::Info) << "Saved " << count << " exterior local map textures"; + } +} diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp new file mode 100644 index 0000000000..59013f4227 --- /dev/null +++ b/apps/openmw/mapextractor.hpp @@ -0,0 +1,54 @@ +#ifndef OPENMW_APPS_OPENMW_MAPEXTRACTOR_HPP +#define OPENMW_APPS_OPENMW_MAPEXTRACTOR_HPP + +#include +#include +#include + +namespace osg +{ + class Group; +} + +namespace SceneUtil +{ + class WorkQueue; +} + +namespace MWRender +{ + class GlobalMap; + class LocalMap; +} + +namespace MWWorld +{ + class World; +} + +namespace OMW +{ + class MapExtractor + { + public: + MapExtractor(MWWorld::World& world, const std::string& worldMapOutput, const std::string& localMapOutput); + ~MapExtractor(); + + void extractWorldMap(); + void extractLocalMaps(); + + private: + MWWorld::World& mWorld; + std::filesystem::path mWorldMapOutputDir; + std::filesystem::path mLocalMapOutputDir; + + std::unique_ptr mGlobalMap; + std::unique_ptr mLocalMap; + + void saveWorldMapTexture(); + void saveWorldMapInfo(); + void saveLocalMapTextures(); + }; +} + +#endif From 3dd3d345431ebb1905c996963fb8bc4f763b352e Mon Sep 17 00:00:00 2001 From: Diject Date: Sat, 27 Dec 2025 20:03:00 +0300 Subject: [PATCH 04/23] Add local map texture extraction for starting cell --- apps/openmw/engine.cpp | 33 ++- apps/openmw/mapextractor.cpp | 411 ++++++++++++++++++++++++++++-- apps/openmw/mapextractor.hpp | 22 +- apps/openmw/mwrender/localmap.cpp | 23 ++ 4 files changed, 452 insertions(+), 37 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 8bd0aad8e1..65db4d668e 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -960,20 +960,6 @@ void OMW::Engine::go() prepareEngine(); - if (mExtractMaps) - { - Log(Debug::Info) << "Starting map extraction mode..."; - - mStateManager->newGame(true); - - MapExtractor extractor(*mWorld, mWorldMapOutput, mLocalMapOutput); - extractor.extractWorldMap(); - extractor.extractLocalMaps(); - - Log(Debug::Info) << "Map extraction complete. Exiting..."; - return; - } - #ifdef _WIN32 const auto* statsFile = _wgetenv(L"OPENMW_OSG_STATS_FILE"); #else @@ -1008,6 +994,25 @@ void OMW::Engine::go() if (stats.is_open()) Resource::collectStatistics(*mViewer); + + // Map extractor + if (mExtractMaps) + { + Log(Debug::Info) << "Starting map extraction mode..."; + + mStateManager->newGame(true); + + Log(Debug::Info) << "Starting map extraction..."; + + MapExtractor extractor(*mWorld, mViewer.get(), mWorldMapOutput, mLocalMapOutput); + extractor.extractWorldMap(); + extractor.extractLocalMaps(); + + Log(Debug::Info) << "Map extraction complete. Exiting..."; + return; + } + + // Start the game if (!mSaveGameFile.empty()) { diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index bfe36ee3ef..6b0bdd8753 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -1,12 +1,17 @@ #include "mapextractor.hpp" +#include +#include #include #include +#include +#include #include #include #include #include +#include #include #include @@ -16,19 +21,23 @@ #include #include "mwbase/environment.hpp" +#include "mwbase/mechanicsmanager.hpp" #include "mwbase/world.hpp" #include "mwrender/globalmap.hpp" #include "mwrender/localmap.hpp" #include "mwrender/renderingmanager.hpp" +#include "mwrender/vismask.hpp" #include "mwworld/cellstore.hpp" #include "mwworld/esmstore.hpp" #include "mwworld/worldimp.hpp" +#include "mwworld/worldmodel.hpp" namespace OMW { - MapExtractor::MapExtractor( - MWWorld::World& world, const std::string& worldMapOutput, const std::string& localMapOutput) + MapExtractor::MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, + const std::string& worldMapOutput, const std::string& localMapOutput) : mWorld(world) + , mViewer(viewer) , mWorldMapOutputDir(worldMapOutput) , mLocalMapOutputDir(localMapOutput) { @@ -37,14 +46,43 @@ namespace OMW // Create GlobalMap and LocalMap instances MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); - if (renderingManager) + if (!renderingManager) { - osg::Group* root = renderingManager->getLightRoot()->getParent(0)->asGroup(); - SceneUtil::WorkQueue* workQueue = renderingManager->getWorkQueue(); + Log(Debug::Error) << "RenderingManager is null in MapExtractor constructor"; + throw std::runtime_error("RenderingManager is null"); + } + osg::Group* lightRoot = renderingManager->getLightRoot(); + if (!lightRoot) + { + Log(Debug::Error) << "LightRoot is null in MapExtractor constructor"; + throw std::runtime_error("LightRoot is null"); + } + + osg::Group* root = lightRoot->getParent(0) ? lightRoot->getParent(0)->asGroup() : nullptr; + if (!root) + { + Log(Debug::Error) << "Root node is null in MapExtractor constructor"; + throw std::runtime_error("Root node is null"); + } + + SceneUtil::WorkQueue* workQueue = renderingManager->getWorkQueue(); + if (!workQueue) + { + Log(Debug::Error) << "WorkQueue is null in MapExtractor constructor"; + throw std::runtime_error("WorkQueue is null"); + } + + try + { mGlobalMap = std::make_unique(root, workQueue); mLocalMap = std::make_unique(root); } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to create map objects: " << e.what(); + throw; + } } MapExtractor::~MapExtractor() = default; @@ -56,7 +94,7 @@ namespace OMW if (!mGlobalMap) { Log(Debug::Error) << "Global map not initialized"; - return; + throw std::runtime_error("Global map not initialized"); } // Temporarily set cell size to 32 pixels for extraction @@ -149,44 +187,375 @@ namespace OMW { Log(Debug::Info) << "Extracting local maps..."; - saveLocalMapTextures(); + setupExtractionMode(); + extractExteriorLocalMaps(); + extractInteriorLocalMaps(); + restoreNormalMode(); Log(Debug::Info) << "Local map extraction complete"; } - void MapExtractor::saveLocalMapTextures() + void MapExtractor::setupExtractionMode() + { + mWorld.toggleCollisionMode(); + MWBase::Environment::get().getMechanicsManager()->toggleAI(); + mWorld.toggleScripts(); + mWorld.toggleGodMode(); + } + + void MapExtractor::restoreNormalMode() + { + if (!mWorld.getGodModeState()) + mWorld.toggleGodMode(); + if (!mWorld.getScriptsEnabled()) + mWorld.toggleScripts(); + if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) + MWBase::Environment::get().getMechanicsManager()->toggleAI(); + } + + void MapExtractor::extractExteriorLocalMaps() { if (!mLocalMap) { Log(Debug::Error) << "Local map not initialized"; - return; + throw std::runtime_error("Local map not initialized"); } - const MWWorld::ESMStore& store = mWorld.getStore(); - int count = 0; - - MWWorld::Store::iterator it = store.get().extBegin(); - for (; it != store.get().extEnd(); ++it) + // Get currently active cells + MWWorld::Scene* scene = &mWorld.getWorldScene(); + if (!scene) { - int x = it->getGridX(); - int y = it->getGridY(); + Log(Debug::Error) << "Scene not available"; + throw std::runtime_error("Scene not available"); + } - osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); - if (!texture || !texture->getImage()) + const auto& activeCells = scene->getActiveCells(); + Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells..."; + + int count = 0; + int skipped = 0; + + for (const MWWorld::CellStore* cellStore : activeCells) + { + if (!cellStore->getCell()->isExterior()) continue; + int x = cellStore->getCell()->getGridX(); + int y = cellStore->getCell()->getGridY(); + + Log(Debug::Info) << "Processing active cell (" << x << "," << y << ")"; + + // Request map generation for this cell + Log(Debug::Verbose) << "Requesting map for cell (" << x << "," << y << ")"; + mLocalMap->requestMap(const_cast(cellStore)); + Log(Debug::Verbose) << "Map requested for cell (" << x << "," << y << ")"; + + // CRITICAL: LocalMap::requestMap() creates RTT cameras that render asynchronously. + // We must run the render loop to actually execute the RTT rendering before we can + // access the resulting textures. + + MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); + if (renderingManager && mViewer) + { + Log(Debug::Verbose) << "Starting render loop for cell (" << x << "," << y << ")"; + + // Phase 1: Setup (let RTT cameras initialize) + for (int i = 0; i < 5; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 2: Main rendering (RTT cameras execute) + for (int i = 0; i < 60; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 3: Finalization (ensure GPU->CPU transfer completes) + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + for (int i = 0; i < 10; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + Log(Debug::Verbose) << "Render loop completed for cell (" << x << "," << y << ")"; + } + + // Clean up RTT cameras before trying to access textures + mLocalMap->cleanupCameras(); + + // Now try to get the texture + Log(Debug::Verbose) << "Getting texture for cell (" << x << "," << y << ")"; + osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); + + if (!texture) + { + Log(Debug::Warning) << "No texture for cell (" << x << "," << y << ")"; + skipped++; + continue; + } + + // Get the image from the texture (should be set by LocalMapRenderToTexture) + osg::Image* image = texture->getImage(); + + if (!image) + { + Log(Debug::Warning) << "Texture for cell (" << x << "," << y << ") has no image data attached"; + skipped++; + continue; + } + + if (image->s() == 0 || image->t() == 0) + { + Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ")"; + skipped++; + continue; + } + + Log(Debug::Info) << "Got image size: " << image->s() << "x" << image->t() + << " for cell (" << x << "," << y << ")"; + + osg::ref_ptr outputImage = new osg::Image(*image, osg::CopyOp::DEEP_COPY_ALL); + + if (outputImage->s() != 256 || outputImage->t() != 256) + { + osg::ref_ptr resized = new osg::Image; + resized->allocateImage(256, 256, 1, outputImage->getPixelFormat(), outputImage->getDataType()); + outputImage->scaleImage(256, 256, 1); + outputImage = resized; + } + std::ostringstream filename; filename << "(" << x << "," << y << ").png"; std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); - if (osgDB::writeImageFile(*texture->getImage(), outputPath.string())) + if (osgDB::writeImageFile(*outputImage, outputPath.string())) { count++; - if (count % 100 == 0) - Log(Debug::Info) << "Saved " << count << " local map textures..."; + Log(Debug::Info) << "Saved local map texture for cell (" << x << "," << y << ")"; + } + else + { + Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ")"; + skipped++; } } Log(Debug::Info) << "Saved " << count << " exterior local map textures"; + if (skipped > 0) + Log(Debug::Warning) << "Skipped " << skipped << " cells without valid textures"; + } + + void MapExtractor::extractInteriorLocalMaps() + { + if (!mLocalMap) + { + Log(Debug::Error) << "Local map not initialized"; + throw std::runtime_error("Local map not initialized"); + } + + // Get currently active cells + MWWorld::Scene* scene = &mWorld.getWorldScene(); + if (!scene) + { + Log(Debug::Error) << "Scene not available"; + throw std::runtime_error("Scene not available"); + } + + const auto& activeCells = scene->getActiveCells(); + Log(Debug::Info) << "Processing active interior cells..."; + + int count = 0; + + for (const MWWorld::CellStore* cellStore : activeCells) + { + if (cellStore->getCell()->isExterior()) + continue; + + ESM::RefId cellId = cellStore->getCell()->getId(); + std::string cellName(cellStore->getCell()->getNameId()); + + Log(Debug::Info) << "Processing active interior cell: " << cellName; + + // Request map generation for this cell + mLocalMap->requestMap(const_cast(cellStore)); + + // CRITICAL: LocalMap::requestMap() creates RTT cameras that render asynchronously. + // We must run the render loop to actually execute the RTT rendering before we can + // access the resulting textures. + + MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); + if (renderingManager && mViewer) + { + Log(Debug::Verbose) << "Starting render loop for interior: " << cellName; + + // Phase 1: Setup (let RTT cameras initialize) + for (int i = 0; i < 5; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 2: Main rendering (RTT cameras execute) + for (int i = 0; i < 60; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 3: Finalization (ensure GPU->CPU transfer completes) + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + for (int i = 0; i < 10; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + Log(Debug::Verbose) << "Render loop completed for interior: " << cellName; + } + + // Clean up RTT cameras before trying to access textures + mLocalMap->cleanupCameras(); + + saveInteriorCellTextures(cellId, cellName); + count++; + } + + Log(Debug::Info) << "Saved " << count << " interior local map textures"; + } + + void MapExtractor::saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName) + { + MyGUI::IntRect grid = mLocalMap->getInteriorGrid(); + + std::string lowerCaseId = cellId.toDebugString(); + std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); + + int segmentsX = grid.width() + 1; + int segmentsY = grid.height() + 1; + + if (segmentsX <= 0 || segmentsY <= 0) + return; + + int totalWidth = segmentsX * 256; + int totalHeight = segmentsY * 256; + + osg::ref_ptr combinedImage = new osg::Image; + combinedImage->allocateImage(totalWidth, totalHeight, 1, GL_RGB, GL_UNSIGNED_BYTE); + + unsigned char* data = combinedImage->data(); + memset(data, 0, totalWidth * totalHeight * 3); + + for (int x = grid.left; x < grid.right; ++x) + { + for (int y = grid.top; y < grid.bottom; ++y) + { + osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); + if (!texture || !texture->getImage()) + continue; + + osg::Image* segmentImage = texture->getImage(); + int segWidth = segmentImage->s(); + int segHeight = segmentImage->t(); + + int destX = (x - grid.left) * 256; + int destY = (y - grid.top) * 256; + + for (int sy = 0; sy < std::min(segHeight, 256); ++sy) + { + for (int sx = 0; sx < std::min(segWidth, 256); ++sx) + { + unsigned char* srcPixel = segmentImage->data(sx, sy); + int dx = destX + sx; + int dy = destY + sy; + + if (dx < totalWidth && dy < totalHeight) + { + unsigned char* destPixel = data + ((dy * totalWidth + dx) * 3); + destPixel[0] = srcPixel[0]; + destPixel[1] = srcPixel[1]; + destPixel[2] = srcPixel[2]; + } + } + } + } + } + + std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); + osgDB::writeImageFile(*combinedImage, texturePath.string()); + + saveInteriorMapInfo(cellId, lowerCaseId, segmentsX, segmentsY); + } + + void MapExtractor::saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, + int segmentsX, int segmentsY) + { + MWWorld::CellStore* cell = mWorld.getWorldModel().findCell(cellId); + if (!cell) + return; + + float nA = 0.0f; + float mX = 0.0f; + float mY = 0.0f; + + MWWorld::ConstPtr northmarker = cell->searchConst(ESM::RefId::stringRefId("northmarker")); + if (!northmarker.isEmpty()) + { + osg::Quat orient(-northmarker.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, 1)); + osg::Vec3f dir = orient * osg::Vec3f(0, 1, 0); + nA = std::atan2(dir.x(), dir.y()); + } + + osg::BoundingBox bounds; + osg::ComputeBoundsVisitor computeBoundsVisitor; + computeBoundsVisitor.setTraversalMask(MWRender::Mask_Scene | MWRender::Mask_Terrain | + MWRender::Mask_Object | MWRender::Mask_Static); + + MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); + if (renderingManager && renderingManager->getLightRoot()) + { + renderingManager->getLightRoot()->accept(computeBoundsVisitor); + bounds = computeBoundsVisitor.getBoundingBox(); + } + + osg::Vec2f center(bounds.center().x(), bounds.center().y()); + osg::Vec2f min(bounds.xMin(), bounds.yMin()); + + const float mapWorldSize = Constants::CellSizeInUnits; + + mX = ((0 - center.x()) * std::cos(nA) - (0 - center.y()) * std::sin(nA) + center.x() - min.x()) / mapWorldSize * 256.0f * 2.0f; + mY = ((0 - center.x()) * std::sin(nA) + (0 - center.y()) * std::cos(nA) + center.y() - min.y()) / mapWorldSize * 256.0f * 2.0f; + + std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); + std::ofstream file(yamlPath); + + if (!file) + { + Log(Debug::Error) << "Failed to create interior map info file: " << yamlPath; + return; + } + + file << "nA: " << nA << "\n"; + file << "mX: " << mX << "\n"; + file << "mY: " << mY << "\n"; + + file.close(); } } diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 59013f4227..9410c67742 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -5,11 +5,18 @@ #include #include +#include + namespace osg { class Group; } +namespace osgViewer +{ + class Viewer; +} + namespace SceneUtil { class WorkQueue; @@ -31,7 +38,8 @@ namespace OMW class MapExtractor { public: - MapExtractor(MWWorld::World& world, const std::string& worldMapOutput, const std::string& localMapOutput); + MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, const std::string& worldMapOutput, + const std::string& localMapOutput); ~MapExtractor(); void extractWorldMap(); @@ -39,6 +47,7 @@ namespace OMW private: MWWorld::World& mWorld; + osgViewer::Viewer* mViewer; std::filesystem::path mWorldMapOutputDir; std::filesystem::path mLocalMapOutputDir; @@ -47,7 +56,16 @@ namespace OMW void saveWorldMapTexture(); void saveWorldMapInfo(); - void saveLocalMapTextures(); + + void setupExtractionMode(); + void restoreNormalMode(); + void extractExteriorLocalMaps(); + void extractInteriorLocalMaps(); + void loadCellAndWait(int x, int y); + void loadInteriorCellAndWait(const std::string& cellName); + void saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName); + void saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, + int segmentsX, int segmentsY); }; } diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 39d088084f..3546c29852 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -774,6 +774,29 @@ namespace MWRender camera->addChild(lightSource); camera->addChild(mSceneRoot); + + // CRITICAL FIX: Setup both texture and image for CPU-side access (needed by mapextractor) + // First attach texture for normal rendering (GPU-side) + osg::ref_ptr texture = new osg::Texture2D(); + texture->setTextureSize(camera->getViewport()->width(), camera->getViewport()->height()); + texture->setInternalFormat(GL_RGB); + texture->setSourceFormat(GL_RGB); + texture->setSourceType(GL_UNSIGNED_BYTE); + texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + camera->attach(osg::Camera::COLOR_BUFFER, texture.get()); + + // Then attach Image for CPU-side readback + // OSG will automatically copy rendered pixels to this Image + osg::ref_ptr image = new osg::Image(); + image->setPixelFormat(GL_RGB); + image->setDataType(GL_UNSIGNED_BYTE); + camera->attach(osg::Camera::COLOR_BUFFER, image.get()); + + // Also set the Image on the texture so mapextractor can retrieve it via texture->getImage() + texture->setImage(image.get()); } void CameraLocalUpdateCallback::operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv) From ea2048e24375745b80302c48099fb5d130788f3e Mon Sep 17 00:00:00 2001 From: Diject Date: Sun, 28 Dec 2025 09:50:45 +0300 Subject: [PATCH 05/23] Add local map extraction lua api --- apps/openmw/engine.cpp | 6 +- apps/openmw/mapextractor.cpp | 187 ++++++++-------------- apps/openmw/mapextractor.hpp | 16 +- apps/openmw/mwbase/windowmanager.hpp | 8 + apps/openmw/mwbase/world.hpp | 3 + apps/openmw/mwgui/windowmanagerimp.cpp | 5 + apps/openmw/mwgui/windowmanagerimp.hpp | 3 + apps/openmw/mwlua/worldbindings.cpp | 41 +++++ apps/openmw/mwrender/renderingmanager.hpp | 1 + apps/openmw/mwworld/worldimp.cpp | 24 +++ apps/openmw/mwworld/worldimp.hpp | 2 + files/lua_api/openmw/world.lua | 22 +++ 12 files changed, 186 insertions(+), 132 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 65db4d668e..0d55bedcab 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -1004,12 +1004,12 @@ void OMW::Engine::go() Log(Debug::Info) << "Starting map extraction..."; - MapExtractor extractor(*mWorld, mViewer.get(), mWorldMapOutput, mLocalMapOutput); + MapExtractor extractor(*mWorld, mViewer.get(), mWindowManager.get(), mWorldMapOutput, mLocalMapOutput); extractor.extractWorldMap(); - extractor.extractLocalMaps(); + //extractor.extractLocalMaps(); Log(Debug::Info) << "Map extraction complete. Exiting..."; - return; + //return; } diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index 6b0bdd8753..97b9e619e3 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -22,6 +22,7 @@ #include "mwbase/environment.hpp" #include "mwbase/mechanicsmanager.hpp" +#include "mwbase/windowmanager.hpp" #include "mwbase/world.hpp" #include "mwrender/globalmap.hpp" #include "mwrender/localmap.hpp" @@ -35,16 +36,18 @@ namespace OMW { MapExtractor::MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, - const std::string& worldMapOutput, const std::string& localMapOutput) + MWBase::WindowManager* windowManager, const std::string& worldMapOutput, const std::string& localMapOutput) : mWorld(world) , mViewer(viewer) + , mWindowManager(windowManager) , mWorldMapOutputDir(worldMapOutput) , mLocalMapOutputDir(localMapOutput) + , mLocalMap(nullptr) { std::filesystem::create_directories(mWorldMapOutputDir); std::filesystem::create_directories(mLocalMapOutputDir); - // Create GlobalMap and LocalMap instances + // Create GlobalMap instance MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); if (!renderingManager) { @@ -76,7 +79,9 @@ namespace OMW try { mGlobalMap = std::make_unique(root, workQueue); - mLocalMap = std::make_unique(root); + // Get LocalMap from WindowManager - it will be set after initUI is called + // For now just set to nullptr + mLocalMap = nullptr; } catch (const std::exception& e) { @@ -185,32 +190,24 @@ namespace OMW void MapExtractor::extractLocalMaps() { - Log(Debug::Info) << "Extracting local maps..."; + Log(Debug::Info) << "Extracting active local maps..."; + + // Get LocalMap from WindowManager now that UI is initialized + if (mWindowManager) + { + mLocalMap = mWindowManager->getLocalMapRender(); + } + + if (!mLocalMap) + { + Log(Debug::Error) << "Local map not initialized - cannot extract local maps"; + return; + } - setupExtractionMode(); extractExteriorLocalMaps(); extractInteriorLocalMaps(); - restoreNormalMode(); - Log(Debug::Info) << "Local map extraction complete"; - } - - void MapExtractor::setupExtractionMode() - { - mWorld.toggleCollisionMode(); - MWBase::Environment::get().getMechanicsManager()->toggleAI(); - mWorld.toggleScripts(); - mWorld.toggleGodMode(); - } - - void MapExtractor::restoreNormalMode() - { - if (!mWorld.getGodModeState()) - mWorld.toggleGodMode(); - if (!mWorld.getScriptsEnabled()) - mWorld.toggleScripts(); - if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) - MWBase::Environment::get().getMechanicsManager()->toggleAI(); + Log(Debug::Info) << "Extraction of active local maps complete"; } void MapExtractor::extractExteriorLocalMaps() @@ -250,50 +247,6 @@ namespace OMW mLocalMap->requestMap(const_cast(cellStore)); Log(Debug::Verbose) << "Map requested for cell (" << x << "," << y << ")"; - // CRITICAL: LocalMap::requestMap() creates RTT cameras that render asynchronously. - // We must run the render loop to actually execute the RTT rendering before we can - // access the resulting textures. - - MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); - if (renderingManager && mViewer) - { - Log(Debug::Verbose) << "Starting render loop for cell (" << x << "," << y << ")"; - - // Phase 1: Setup (let RTT cameras initialize) - for (int i = 0; i < 5; ++i) - { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - renderingManager->update(0.016f, false); - mViewer->renderingTraversals(); - } - - // Phase 2: Main rendering (RTT cameras execute) - for (int i = 0; i < 60; ++i) - { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - renderingManager->update(0.016f, false); - mViewer->renderingTraversals(); - } - - // Phase 3: Finalization (ensure GPU->CPU transfer completes) - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - - for (int i = 0; i < 10; ++i) - { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - renderingManager->update(0.016f, false); - mViewer->renderingTraversals(); - } - - Log(Debug::Verbose) << "Render loop completed for cell (" << x << "," << y << ")"; - } - - // Clean up RTT cameras before trying to access textures - mLocalMap->cleanupCameras(); - // Now try to get the texture Log(Debug::Verbose) << "Getting texture for cell (" << x << "," << y << ")"; osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); @@ -387,53 +340,6 @@ namespace OMW Log(Debug::Info) << "Processing active interior cell: " << cellName; - // Request map generation for this cell - mLocalMap->requestMap(const_cast(cellStore)); - - // CRITICAL: LocalMap::requestMap() creates RTT cameras that render asynchronously. - // We must run the render loop to actually execute the RTT rendering before we can - // access the resulting textures. - - MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); - if (renderingManager && mViewer) - { - Log(Debug::Verbose) << "Starting render loop for interior: " << cellName; - - // Phase 1: Setup (let RTT cameras initialize) - for (int i = 0; i < 5; ++i) - { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - renderingManager->update(0.016f, false); - mViewer->renderingTraversals(); - } - - // Phase 2: Main rendering (RTT cameras execute) - for (int i = 0; i < 60; ++i) - { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - renderingManager->update(0.016f, false); - mViewer->renderingTraversals(); - } - - // Phase 3: Finalization (ensure GPU->CPU transfer completes) - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - - for (int i = 0; i < 10; ++i) - { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - renderingManager->update(0.016f, false); - mViewer->renderingTraversals(); - } - - Log(Debug::Verbose) << "Render loop completed for interior: " << cellName; - } - - // Clean up RTT cameras before trying to access textures - mLocalMap->cleanupCameras(); - saveInteriorCellTextures(cellId, cellName); count++; } @@ -448,8 +354,43 @@ namespace OMW std::string lowerCaseId = cellId.toDebugString(); std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); - int segmentsX = grid.width() + 1; - int segmentsY = grid.height() + 1; + const std::string invalidChars = "/\\:*?\"<>|"; + lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), + [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), + lowerCaseId.end() + ); + + if (lowerCaseId.empty()) + { + lowerCaseId = "_unnamed_cell_"; + } + + + int minX = grid.right; + int maxX = grid.left; + int minY = grid.bottom; + int maxY = grid.top; + + for (int x = grid.left; x < grid.right; ++x) + { + for (int y = grid.top; y < grid.bottom; ++y) + { + osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); + if (texture && texture->getImage()) + { + minX = std::min(minX, x); + maxX = std::max(maxX, x); + minY = std::min(minY, y); + maxY = std::max(maxY, y); + } + } + } + + if (minX > maxX || minY > maxY) + return; + + int segmentsX = maxX - minX + 1; + int segmentsY = maxY - minY + 1; if (segmentsX <= 0 || segmentsY <= 0) return; @@ -463,9 +404,9 @@ namespace OMW unsigned char* data = combinedImage->data(); memset(data, 0, totalWidth * totalHeight * 3); - for (int x = grid.left; x < grid.right; ++x) + for (int x = minX; x <= maxX; ++x) { - for (int y = grid.top; y < grid.bottom; ++y) + for (int y = minY; y <= maxY; ++y) { osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); if (!texture || !texture->getImage()) @@ -475,8 +416,8 @@ namespace OMW int segWidth = segmentImage->s(); int segHeight = segmentImage->t(); - int destX = (x - grid.left) * 256; - int destY = (y - grid.top) * 256; + int destX = (x - minX) * 256; + int destY = (y - minY) * 256; for (int sy = 0; sy < std::min(segHeight, 256); ++sy) { @@ -555,6 +496,8 @@ namespace OMW file << "nA: " << nA << "\n"; file << "mX: " << mX << "\n"; file << "mY: " << mY << "\n"; + file << "width: " << segmentsX << "\n"; + file << "height: " << segmentsY << "\n"; file.close(); } diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 9410c67742..69ccc0038a 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -33,13 +33,18 @@ namespace MWWorld class World; } +namespace MWBase +{ + class WindowManager; +} + namespace OMW { class MapExtractor { public: - MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, const std::string& worldMapOutput, - const std::string& localMapOutput); + MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, MWBase::WindowManager* windowManager, + const std::string& worldMapOutput, const std::string& localMapOutput); ~MapExtractor(); void extractWorldMap(); @@ -48,21 +53,18 @@ namespace OMW private: MWWorld::World& mWorld; osgViewer::Viewer* mViewer; + MWBase::WindowManager* mWindowManager; std::filesystem::path mWorldMapOutputDir; std::filesystem::path mLocalMapOutputDir; std::unique_ptr mGlobalMap; - std::unique_ptr mLocalMap; + MWRender::LocalMap* mLocalMap; void saveWorldMapTexture(); void saveWorldMapInfo(); - void setupExtractionMode(); - void restoreNormalMode(); void extractExteriorLocalMaps(); void extractInteriorLocalMaps(); - void loadCellAndWait(int x, int y); - void loadInteriorCellAndWait(const std::string& cellName); void saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName); void saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, int segmentsX, int segmentsY); diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 8385e85b5f..b902e278a6 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -91,6 +91,11 @@ namespace MWGui struct TextColours; } +namespace MWRender +{ + class LocalMap; +} + namespace SFO { class CursorManager; @@ -406,6 +411,9 @@ namespace MWBase virtual bool isWindowVisible(std::string_view windowId) const = 0; virtual std::vector getAllWindowIds() const = 0; virtual std::vector getAllowedWindowIds(MWGui::GuiMode mode) const = 0; + + // For map extraction + virtual MWRender::LocalMap* getLocalMapRender() = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 23fb98063f..fee2fa92af 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -606,6 +606,9 @@ namespace MWBase virtual MWWorld::DateTimeManager* getTimeManager() = 0; virtual void setActorActive(const MWWorld::Ptr& ptr, bool value) = 0; + + virtual void extractLocalMaps(const std::string& worldMapOutput, const std::string& localMapOutput) = 0; + ///< Extract local and world maps to the specified directories }; } diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index ffc0705b03..e53c0a5b39 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -2697,4 +2697,9 @@ namespace MWGui else mInventoryTabsOverlay->setVisible(false); } + + MWRender::LocalMap* WindowManager::getLocalMapRender() + { + return mLocalMapRender.get(); + } } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 03f0ea5572..fb9e8a8e80 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -409,6 +409,9 @@ namespace MWGui std::vector getAllWindowIds() const override; std::vector getAllowedWindowIds(GuiMode mode) const override; + // For map extraction + MWRender::LocalMap* getLocalMapRender() override; + private: unsigned int mOldUpdateMask; unsigned int mOldCullMask; diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index ae7ede69d2..fd7dd4ae38 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -247,6 +247,47 @@ namespace MWLua api["vfx"] = initWorldVfxBindings(context); + api["extractLocalMaps"] = [context, lua = context.mLua]( + const std::string& worldMapOutput, const std::string& localMapOutput) { + checkGameInitialized(lua); + context.mLuaManager->addAction( + [worldMapOutput, localMapOutput] { + MWBase::Environment::get().getWorld()->extractLocalMaps(worldMapOutput, localMapOutput); + }, + "extractLocalMapsAction"); + }; + + api["enableExtractionMode"] = [context, lua = context.mLua]() { + checkGameInitialized(lua); + context.mLuaManager->addAction( + [] { + auto world = MWBase::Environment::get().getWorld(); + world->toggleCollisionMode(); + MWBase::Environment::get().getMechanicsManager()->toggleAI(); + world->toggleScripts(); + world->toggleGodMode(); + world->toggleVanityMode(false); + }, + "enableExtractionModeAction"); + }; + + api["disableExtractionMode"] = [context, lua = context.mLua]() { + checkGameInitialized(lua); + context.mLuaManager->addAction( + [] { + auto world = MWBase::Environment::get().getWorld(); + if (!world->getGodModeState()) + world->toggleGodMode(); + if (!world->getScriptsEnabled()) + world->toggleScripts(); + if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) + MWBase::Environment::get().getMechanicsManager()->toggleAI(); + world->toggleCollisionMode(); + world->toggleVanityMode(false); + }, + "disableExtractionModeAction"); + }; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index f9ff9f5945..6392ec8289 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -237,6 +237,7 @@ namespace MWRender // camera stuff Camera* getCamera() { return mCamera.get(); } + osgViewer::Viewer* getViewer() { return mViewer.get(); } /// temporarily override the field of view with given value. void overrideFieldOfView(float val); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index fcb479db45..bf238d0ac1 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -93,6 +93,8 @@ #include "../mwsound/constants.hpp" +#include "../mapextractor.hpp" + #include "actionteleport.hpp" #include "cellstore.hpp" #include "containerstore.hpp" @@ -3899,4 +3901,26 @@ namespace MWWorld if (MWPhysics::Actor* const actor = mPhysics->getActor(ptr)) actor->setActive(value); } + + void World::extractLocalMaps(const std::string& worldMapOutput, const std::string& localMapOutput) + { + if (!mRendering) + { + throw std::runtime_error("Rendering manager is not initialized"); + } + + osgViewer::Viewer* viewer = mRendering->getViewer(); + if (!viewer) + { + throw std::runtime_error("Viewer is not initialized"); + } + + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + OMW::MapExtractor extractor(*this, viewer, windowManager, worldMapOutput, localMapOutput); + + Log(Debug::Info) << "Starting map extraction..."; + extractor.extractWorldMap(); + extractor.extractLocalMaps(); + Log(Debug::Info) << "Map extraction complete"; + } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 4b8d5def1b..878134904e 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -680,6 +680,8 @@ namespace MWWorld DateTimeManager* getTimeManager() override { return mTimeManager.get(); } void setActorActive(const MWWorld::Ptr& ptr, bool value) override; + + void extractLocalMaps(const std::string& worldMapOutput, const std::string& localMapOutput) override; }; } diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 0cc1d065ff..0ebc56d3b5 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -221,4 +221,26 @@ -- @function [parent=#world] advanceTime -- @param #number hours Number of hours to advance time +--- +-- Extract local and world maps to the specified directories. +-- This function generates map images for all active cells and saves them as PNG files. +-- @function [parent=#world] extractLocalMaps +-- @param #string worldMapOutput Directory path where world map will be saved +-- @param #string localMapOutput Directory path where local maps will be saved +-- @usage world.extractLocalMaps("./maps/world", "./maps/local") + +--- +-- Enable extraction mode for map generation. +-- This mode disables collision, AI, scripts, and enables god mode to facilitate map extraction. +-- Should be called before extractLocalMaps to prepare the game state. +-- @function [parent=#world] enableExtractionMode +-- @usage world.enableExtractionMode() + +--- +-- Disable extraction mode and restore normal game behavior. +-- Restores god mode, scripts, and AI to their normal state. +-- Should be called after map extraction is complete. +-- @function [parent=#world] disableExtractionMode +-- @usage world.disableExtractionMode() + return nil From 3ac33b86e5d64ffbc4da24dc08ae6b671ec34050 Mon Sep 17 00:00:00 2001 From: Diject Date: Sun, 28 Dec 2025 12:32:09 +0300 Subject: [PATCH 06/23] Disable navmesh updater worker threads by default --- components/detournavigator/asyncnavmeshupdater.cpp | 4 ++++ components/detournavigator/settings.cpp | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index d7eaafce9a..b8b24fa696 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -348,6 +348,10 @@ namespace DetourNavigator void AsyncNavMeshUpdater::wait(WaitConditionType waitConditionType, Loading::Listener* listener) { + // If there are no worker threads, jobs will never be processed, so don't wait + if (mThreads.empty()) + return; + switch (waitConditionType) { case WaitConditionType::requiredTilesPresent: diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp index d71b3d12bc..d66040b48d 100644 --- a/components/detournavigator/settings.cpp +++ b/components/detournavigator/settings.cpp @@ -94,7 +94,6 @@ namespace DetourNavigator result.mMaxTilesNumber = std::min(limits.mMaxTiles, ::Settings::navigator().mMaxTilesNumber.get()); result.mWaitUntilMinDistanceToPlayer = ::Settings::navigator().mWaitUntilMinDistanceToPlayer; - result.mAsyncNavMeshUpdaterThreads = ::Settings::navigator().mAsyncNavMeshUpdaterThreads; result.mMaxNavMeshTilesCacheSize = ::Settings::navigator().mMaxNavMeshTilesCacheSize; result.mEnableWriteRecastMeshToFile = ::Settings::navigator().mEnableWriteRecastMeshToFile; result.mEnableWriteNavMeshToFile = ::Settings::navigator().mEnableWriteNavMeshToFile; @@ -106,6 +105,9 @@ namespace DetourNavigator result.mEnableNavMeshDiskCache = ::Settings::navigator().mEnableNavMeshDiskCache; result.mWriteToNavMeshDb = ::Settings::navigator().mWriteToNavmeshdb; result.mMaxDbFileSize = ::Settings::navigator().mMaxNavmeshdbFileSize; + + // Force disable navmesh generation worker threads regardless of config file settings + result.mAsyncNavMeshUpdaterThreads = 0; return result; } From 980c821cab01c1d5592d92660b2678be4aaf3ede Mon Sep 17 00:00:00 2001 From: Diject Date: Sun, 28 Dec 2025 12:50:26 +0300 Subject: [PATCH 07/23] Add forceOverwrite option to local map extraction --- apps/openmw/mapextractor.cpp | 57 +++++++++++++++++++++++++++++------- apps/openmw/mapextractor.hpp | 6 ++-- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index 97b9e619e3..fc46301f9b 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -188,7 +188,7 @@ namespace OMW Log(Debug::Info) << "Saved world map info: " << infoPath; } - void MapExtractor::extractLocalMaps() + void MapExtractor::extractLocalMaps(bool forceOverwrite) { Log(Debug::Info) << "Extracting active local maps..."; @@ -204,13 +204,13 @@ namespace OMW return; } - extractExteriorLocalMaps(); - extractInteriorLocalMaps(); + extractExteriorLocalMaps(forceOverwrite); + extractInteriorLocalMaps(forceOverwrite); Log(Debug::Info) << "Extraction of active local maps complete"; } - void MapExtractor::extractExteriorLocalMaps() + void MapExtractor::extractExteriorLocalMaps(bool forceOverwrite) { if (!mLocalMap) { @@ -240,6 +240,18 @@ namespace OMW int x = cellStore->getCell()->getGridX(); int y = cellStore->getCell()->getGridY(); + // Check if file already exists + std::ostringstream filename; + filename << "(" << x << "," << y << ").png"; + std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); + + if (!forceOverwrite && std::filesystem::exists(outputPath)) + { + Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; + skipped++; + continue; + } + Log(Debug::Info) << "Processing active cell (" << x << "," << y << ")"; // Request map generation for this cell @@ -288,10 +300,6 @@ namespace OMW outputImage = resized; } - std::ostringstream filename; - filename << "(" << x << "," << y << ").png"; - std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); - if (osgDB::writeImageFile(*outputImage, outputPath.string())) { count++; @@ -306,10 +314,10 @@ namespace OMW Log(Debug::Info) << "Saved " << count << " exterior local map textures"; if (skipped > 0) - Log(Debug::Warning) << "Skipped " << skipped << " cells without valid textures"; + Log(Debug::Warning) << "Skipped " << skipped << " cells (already exist or without valid textures)"; } - void MapExtractor::extractInteriorLocalMaps() + void MapExtractor::extractInteriorLocalMaps(bool forceOverwrite) { if (!mLocalMap) { @@ -329,6 +337,7 @@ namespace OMW Log(Debug::Info) << "Processing active interior cells..."; int count = 0; + int skipped = 0; for (const MWWorld::CellStore* cellStore : activeCells) { @@ -338,6 +347,32 @@ namespace OMW ESM::RefId cellId = cellStore->getCell()->getId(); std::string cellName(cellStore->getCell()->getNameId()); + // Prepare lowercase ID for file naming + std::string lowerCaseId = cellId.toDebugString(); + std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); + + const std::string invalidChars = "/\\:*?\"<>|"; + lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), + [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), + lowerCaseId.end() + ); + + if (lowerCaseId.empty()) + { + lowerCaseId = "_unnamed_cell_"; + } + + // Check if both files exist + std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); + std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); + + if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) + { + Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; + skipped++; + continue; + } + Log(Debug::Info) << "Processing active interior cell: " << cellName; saveInteriorCellTextures(cellId, cellName); @@ -345,6 +380,8 @@ namespace OMW } Log(Debug::Info) << "Saved " << count << " interior local map textures"; + if (skipped > 0) + Log(Debug::Info) << "Skipped " << skipped << " interior cells (files already exist)"; } void MapExtractor::saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName) diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 69ccc0038a..620ba3d439 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -48,7 +48,7 @@ namespace OMW ~MapExtractor(); void extractWorldMap(); - void extractLocalMaps(); + void extractLocalMaps(bool forceOverwrite = false); private: MWWorld::World& mWorld; @@ -63,8 +63,8 @@ namespace OMW void saveWorldMapTexture(); void saveWorldMapInfo(); - void extractExteriorLocalMaps(); - void extractInteriorLocalMaps(); + void extractExteriorLocalMaps(bool forceOverwrite = false); + void extractInteriorLocalMaps(bool forceOverwrite = false); void saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName); void saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, int segmentsX, int segmentsY); From a4f242358e0ba349e1c77b9d7cc7d6038c9220fe Mon Sep 17 00:00:00 2001 From: Diject Date: Sun, 28 Dec 2025 14:31:15 +0300 Subject: [PATCH 08/23] Refactor map extraction API and add output path options --- apps/openmw/mapextractor.cpp | 25 +++++++++++++-- apps/openmw/mwbase/world.hpp | 13 ++++++-- apps/openmw/mwlua/worldbindings.cpp | 18 ++++++++--- apps/openmw/mwworld/worldimp.cpp | 48 ++++++++++++++++++++++++++--- apps/openmw/mwworld/worldimp.hpp | 6 +++- files/lua_api/openmw/world.lua | 20 +++++++++--- 6 files changed, 112 insertions(+), 18 deletions(-) diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index fc46301f9b..894c13fe99 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -44,8 +44,11 @@ namespace OMW , mLocalMapOutputDir(localMapOutput) , mLocalMap(nullptr) { - std::filesystem::create_directories(mWorldMapOutputDir); - std::filesystem::create_directories(mLocalMapOutputDir); + // Only create directories if paths are not empty + if (!mWorldMapOutputDir.empty()) + std::filesystem::create_directories(mWorldMapOutputDir); + if (!mLocalMapOutputDir.empty()) + std::filesystem::create_directories(mLocalMapOutputDir); // Create GlobalMap instance MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); @@ -96,6 +99,15 @@ namespace OMW { Log(Debug::Info) << "Extracting world map..."; + if (mWorldMapOutputDir.empty()) + { + Log(Debug::Warning) << "World map output directory is not set, skipping world map extraction"; + return; + } + + // Create output directory if it doesn't exist + std::filesystem::create_directories(mWorldMapOutputDir); + if (!mGlobalMap) { Log(Debug::Error) << "Global map not initialized"; @@ -192,6 +204,15 @@ namespace OMW { Log(Debug::Info) << "Extracting active local maps..."; + if (mLocalMapOutputDir.empty()) + { + Log(Debug::Warning) << "Local map output directory is not set, skipping local map extraction"; + return; + } + + // Create output directory if it doesn't exist + std::filesystem::create_directories(mLocalMapOutputDir); + // Get LocalMap from WindowManager now that UI is initialized if (mWindowManager) { diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index fee2fa92af..969be697a4 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -607,8 +607,17 @@ namespace MWBase virtual void setActorActive(const MWWorld::Ptr& ptr, bool value) = 0; - virtual void extractLocalMaps(const std::string& worldMapOutput, const std::string& localMapOutput) = 0; - ///< Extract local and world maps to the specified directories + virtual std::string getWorldMapOutputPath() const = 0; + ///< Get the world map output path from options or default + + virtual std::string getLocalMapOutputPath() const = 0; + ///< Get the local map output path from options or default + + virtual void extractWorldMap(const std::string& worldMapOutput) = 0; + ///< Extract world map to the specified directory + + virtual void extractLocalMaps(const std::string& localMapOutput) = 0; + ///< Extract local maps to the specified directory }; } diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index fd7dd4ae38..234c0cac76 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -247,12 +247,22 @@ namespace MWLua api["vfx"] = initWorldVfxBindings(context); - api["extractLocalMaps"] = [context, lua = context.mLua]( - const std::string& worldMapOutput, const std::string& localMapOutput) { + api["extractWorldMap"] = [context, lua = context.mLua](sol::optional worldMapOutput) { checkGameInitialized(lua); context.mLuaManager->addAction( - [worldMapOutput, localMapOutput] { - MWBase::Environment::get().getWorld()->extractLocalMaps(worldMapOutput, localMapOutput); + [worldMapOutput] { + std::string path = worldMapOutput.value_or(""); + MWBase::Environment::get().getWorld()->extractWorldMap(path); + }, + "extractWorldMapAction"); + }; + + api["extractLocalMaps"] = [context, lua = context.mLua](sol::optional localMapOutput) { + checkGameInitialized(lua); + context.mLuaManager->addAction( + [localMapOutput] { + std::string path = localMapOutput.value_or(""); + MWBase::Environment::get().getWorld()->extractLocalMaps(path); }, "extractLocalMapsAction"); }; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index bf238d0ac1..d83e3f9fa2 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3902,7 +3902,23 @@ namespace MWWorld actor->setActive(value); } - void World::extractLocalMaps(const std::string& worldMapOutput, const std::string& localMapOutput) + std::string World::getWorldMapOutputPath() const + { + // Try to get from Engine via environment, fallback to default + // Since we can't directly access Engine from World, we'll use a default path + // The actual path from options will be passed through the Lua API + return "./textures/advanced_world_map/custom"; + } + + std::string World::getLocalMapOutputPath() const + { + // Try to get from Engine via environment, fallback to default + // Since we can't directly access Engine from World, we'll use a default path + // The actual path from options will be passed through the Lua API + return "./textures/advanced_world_map/local"; + } + + void World::extractWorldMap(const std::string& worldMapOutput) { if (!mRendering) { @@ -3915,12 +3931,36 @@ namespace MWWorld throw std::runtime_error("Viewer is not initialized"); } + std::string outputPath = worldMapOutput.empty() ? getWorldMapOutputPath() : worldMapOutput; + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - OMW::MapExtractor extractor(*this, viewer, windowManager, worldMapOutput, localMapOutput); + OMW::MapExtractor extractor(*this, viewer, windowManager, outputPath, ""); - Log(Debug::Info) << "Starting map extraction..."; + Log(Debug::Info) << "Starting world map extraction to: " << outputPath; extractor.extractWorldMap(); + Log(Debug::Info) << "World map extraction complete"; + } + + void World::extractLocalMaps(const std::string& localMapOutput) + { + if (!mRendering) + { + throw std::runtime_error("Rendering manager is not initialized"); + } + + osgViewer::Viewer* viewer = mRendering->getViewer(); + if (!viewer) + { + throw std::runtime_error("Viewer is not initialized"); + } + + std::string outputPath = localMapOutput.empty() ? getLocalMapOutputPath() : localMapOutput; + + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + OMW::MapExtractor extractor(*this, viewer, windowManager, "", outputPath); + + Log(Debug::Info) << "Starting local maps extraction to: " << outputPath; extractor.extractLocalMaps(); - Log(Debug::Info) << "Map extraction complete"; + Log(Debug::Info) << "Local maps extraction complete"; } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 878134904e..71c1ee6203 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -681,7 +681,11 @@ namespace MWWorld void setActorActive(const MWWorld::Ptr& ptr, bool value) override; - void extractLocalMaps(const std::string& worldMapOutput, const std::string& localMapOutput) override; + std::string getWorldMapOutputPath() const override; + std::string getLocalMapOutputPath() const override; + + void extractWorldMap(const std::string& worldMapOutput) override; + void extractLocalMaps(const std::string& localMapOutput) override; }; } diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 0ebc56d3b5..c89eb3b0d6 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -222,17 +222,27 @@ -- @param #number hours Number of hours to advance time --- --- Extract local and world maps to the specified directories. +-- Extract world map to the specified directory. +-- This function generates a world map image and saves it as a PNG file. +-- If no path is provided (nil), uses the path from --world-map-output option or default "./textures/advanced_world_map/custom". +-- @function [parent=#world] extractWorldMap +-- @param #string worldMapOutput (optional) Directory path where world map will be saved +-- @usage world.extractWorldMap("./maps/world") -- Custom path +-- @usage world.extractWorldMap() -- Use default or option path + +--- +-- Extract local maps to the specified directory. -- This function generates map images for all active cells and saves them as PNG files. +-- If no path is provided (nil), uses the path from --local-map-output option or default "./textures/advanced_world_map/local". -- @function [parent=#world] extractLocalMaps --- @param #string worldMapOutput Directory path where world map will be saved --- @param #string localMapOutput Directory path where local maps will be saved --- @usage world.extractLocalMaps("./maps/world", "./maps/local") +-- @param #string localMapOutput (optional) Directory path where local maps will be saved +-- @usage world.extractLocalMaps("./maps/local") -- Custom path +-- @usage world.extractLocalMaps() -- Use default or option path --- -- Enable extraction mode for map generation. -- This mode disables collision, AI, scripts, and enables god mode to facilitate map extraction. --- Should be called before extractLocalMaps to prepare the game state. +-- Should be called before extractWorldMap or extractLocalMaps to prepare the game state. -- @function [parent=#world] enableExtractionMode -- @usage world.enableExtractionMode() From dfaea48d73dfdbb5217e06dd6d1f3b1b49b4b80c Mon Sep 17 00:00:00 2001 From: Diject Date: Sun, 28 Dec 2025 17:53:35 +0300 Subject: [PATCH 09/23] Refactor local map extraction for active cells --- apps/openmw/mapextractor.cpp | 290 ++++++++++++++++------------------- apps/openmw/mapextractor.hpp | 5 +- 2 files changed, 136 insertions(+), 159 deletions(-) diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index 894c13fe99..e3261e9af4 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -225,20 +226,6 @@ namespace OMW return; } - extractExteriorLocalMaps(forceOverwrite); - extractInteriorLocalMaps(forceOverwrite); - - Log(Debug::Info) << "Extraction of active local maps complete"; - } - - void MapExtractor::extractExteriorLocalMaps(bool forceOverwrite) - { - if (!mLocalMap) - { - Log(Debug::Error) << "Local map not initialized"; - throw std::runtime_error("Local map not initialized"); - } - // Get currently active cells MWWorld::Scene* scene = &mWorld.getWorldScene(); if (!scene) @@ -250,159 +237,143 @@ namespace OMW const auto& activeCells = scene->getActiveCells(); Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells..."; - int count = 0; - int skipped = 0; - - for (const MWWorld::CellStore* cellStore : activeCells) - { - if (!cellStore->getCell()->isExterior()) - continue; - - int x = cellStore->getCell()->getGridX(); - int y = cellStore->getCell()->getGridY(); - - // Check if file already exists - std::ostringstream filename; - filename << "(" << x << "," << y << ").png"; - std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); - - if (!forceOverwrite && std::filesystem::exists(outputPath)) - { - Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; - skipped++; - continue; - } - - Log(Debug::Info) << "Processing active cell (" << x << "," << y << ")"; - - // Request map generation for this cell - Log(Debug::Verbose) << "Requesting map for cell (" << x << "," << y << ")"; - mLocalMap->requestMap(const_cast(cellStore)); - Log(Debug::Verbose) << "Map requested for cell (" << x << "," << y << ")"; - - // Now try to get the texture - Log(Debug::Verbose) << "Getting texture for cell (" << x << "," << y << ")"; - osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); - - if (!texture) - { - Log(Debug::Warning) << "No texture for cell (" << x << "," << y << ")"; - skipped++; - continue; - } - - // Get the image from the texture (should be set by LocalMapRenderToTexture) - osg::Image* image = texture->getImage(); - - if (!image) - { - Log(Debug::Warning) << "Texture for cell (" << x << "," << y << ") has no image data attached"; - skipped++; - continue; - } - - if (image->s() == 0 || image->t() == 0) - { - Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ")"; - skipped++; - continue; - } - - Log(Debug::Info) << "Got image size: " << image->s() << "x" << image->t() - << " for cell (" << x << "," << y << ")"; - - osg::ref_ptr outputImage = new osg::Image(*image, osg::CopyOp::DEEP_COPY_ALL); - - if (outputImage->s() != 256 || outputImage->t() != 256) - { - osg::ref_ptr resized = new osg::Image; - resized->allocateImage(256, 256, 1, outputImage->getPixelFormat(), outputImage->getDataType()); - outputImage->scaleImage(256, 256, 1); - outputImage = resized; - } - - if (osgDB::writeImageFile(*outputImage, outputPath.string())) - { - count++; - Log(Debug::Info) << "Saved local map texture for cell (" << x << "," << y << ")"; - } - else - { - Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ")"; - skipped++; - } - } - - Log(Debug::Info) << "Saved " << count << " exterior local map textures"; - if (skipped > 0) - Log(Debug::Warning) << "Skipped " << skipped << " cells (already exist or without valid textures)"; - } - - void MapExtractor::extractInteriorLocalMaps(bool forceOverwrite) - { - if (!mLocalMap) - { - Log(Debug::Error) << "Local map not initialized"; - throw std::runtime_error("Local map not initialized"); - } - - // Get currently active cells - MWWorld::Scene* scene = &mWorld.getWorldScene(); - if (!scene) - { - Log(Debug::Error) << "Scene not available"; - throw std::runtime_error("Scene not available"); - } - - const auto& activeCells = scene->getActiveCells(); - Log(Debug::Info) << "Processing active interior cells..."; - - int count = 0; + int exteriorCount = 0; + int interiorCount = 0; int skipped = 0; for (const MWWorld::CellStore* cellStore : activeCells) { if (cellStore->getCell()->isExterior()) - continue; - - ESM::RefId cellId = cellStore->getCell()->getId(); - std::string cellName(cellStore->getCell()->getNameId()); - - // Prepare lowercase ID for file naming - std::string lowerCaseId = cellId.toDebugString(); - std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); - - const std::string invalidChars = "/\\:*?\"<>|"; - lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), - [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), - lowerCaseId.end() - ); - - if (lowerCaseId.empty()) { - lowerCaseId = "_unnamed_cell_"; + if (extractExteriorCell(cellStore, forceOverwrite)) + exteriorCount++; + else + skipped++; } - - // Check if both files exist - std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); - std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); - - if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) + else { - Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; - skipped++; - continue; + if (extractInteriorCell(cellStore, forceOverwrite)) + interiorCount++; + else + skipped++; } - - Log(Debug::Info) << "Processing active interior cell: " << cellName; - - saveInteriorCellTextures(cellId, cellName); - count++; } - Log(Debug::Info) << "Saved " << count << " interior local map textures"; + Log(Debug::Info) << "Saved " << exteriorCount << " exterior local map textures"; + Log(Debug::Info) << "Saved " << interiorCount << " interior local map textures"; if (skipped > 0) - Log(Debug::Info) << "Skipped " << skipped << " interior cells (files already exist)"; + Log(Debug::Info) << "Skipped " << skipped << " cells (files already exist or without valid textures)"; + + Log(Debug::Info) << "Extraction of active local maps complete"; + } + + bool MapExtractor::extractExteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite) + { + int x = cellStore->getCell()->getGridX(); + int y = cellStore->getCell()->getGridY(); + + // Check if file already exists + std::ostringstream filename; + filename << "(" << x << "," << y << ").png"; + std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); + + if (!forceOverwrite && std::filesystem::exists(outputPath)) + { + Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; + return false; + } + + Log(Debug::Info) << "Processing active cell (" << x << "," << y << ")"; + + // Request map generation for this cell + Log(Debug::Verbose) << "Requesting map for cell (" << x << "," << y << ")"; + mLocalMap->requestMap(const_cast(cellStore)); + Log(Debug::Verbose) << "Map requested for cell (" << x << "," << y << ")"; + + // Now try to get the texture + Log(Debug::Verbose) << "Getting texture for cell (" << x << "," << y << ")"; + osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); + + if (!texture) + { + Log(Debug::Warning) << "No texture for cell (" << x << "," << y << ")"; + return false; + } + + // Get the image from the texture (should be set by LocalMapRenderToTexture) + osg::Image* image = texture->getImage(); + + if (!image) + { + Log(Debug::Warning) << "Texture for cell (" << x << "," << y << ") has no image data attached"; + return false; + } + + if (image->s() == 0 || image->t() == 0) + { + Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ")"; + return false; + } + + Log(Debug::Info) << "Got image size: " << image->s() << "x" << image->t() + << " for cell (" << x << "," << y << ")"; + + osg::ref_ptr outputImage = new osg::Image(*image, osg::CopyOp::DEEP_COPY_ALL); + + if (outputImage->s() != 256 || outputImage->t() != 256) + { + osg::ref_ptr resized = new osg::Image; + resized->allocateImage(256, 256, 1, outputImage->getPixelFormat(), outputImage->getDataType()); + outputImage->scaleImage(256, 256, 1); + outputImage = resized; + } + + if (osgDB::writeImageFile(*outputImage, outputPath.string())) + { + Log(Debug::Info) << "Saved local map texture for cell (" << x << "," << y << ")"; + return true; + } + else + { + Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ")"; + return false; + } + } + + bool MapExtractor::extractInteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite) + { + ESM::RefId cellId = cellStore->getCell()->getId(); + std::string cellName(cellStore->getCell()->getNameId()); + + // Prepare lowercase ID for file naming + std::string lowerCaseId = cellId.toDebugString(); + std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); + + const std::string invalidChars = "/\\:*?\"<>|"; + lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), + [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), + lowerCaseId.end() + ); + + if (lowerCaseId.empty()) + { + lowerCaseId = "_unnamed_cell_"; + } + + // Check if both files exist + std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); + std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); + + if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) + { + Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; + return false; + } + + Log(Debug::Info) << "Processing active interior cell: " << cellName; + + saveInteriorCellTextures(cellId, cellName); + return true; } void MapExtractor::saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName) @@ -429,6 +400,9 @@ namespace OMW int minY = grid.bottom; int maxY = grid.top; + // Cache textures and find bounds + std::map, osg::ref_ptr> textureCache; + for (int x = grid.left; x < grid.right; ++x) { for (int y = grid.top; y < grid.bottom; ++y) @@ -436,6 +410,7 @@ namespace OMW osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); if (texture && texture->getImage()) { + textureCache[{x, y}] = texture; minX = std::min(minX, x); maxX = std::max(maxX, x); minY = std::min(minY, y); @@ -466,10 +441,11 @@ namespace OMW { for (int y = minY; y <= maxY; ++y) { - osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); - if (!texture || !texture->getImage()) + auto it = textureCache.find({x, y}); + if (it == textureCache.end()) continue; + osg::ref_ptr texture = it->second; osg::Image* segmentImage = texture->getImage(); int segWidth = segmentImage->s(); int segHeight = segmentImage->t(); diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 620ba3d439..800a08c16a 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -31,6 +31,7 @@ namespace MWRender namespace MWWorld { class World; + class CellStore; } namespace MWBase @@ -63,8 +64,8 @@ namespace OMW void saveWorldMapTexture(); void saveWorldMapInfo(); - void extractExteriorLocalMaps(bool forceOverwrite = false); - void extractInteriorLocalMaps(bool forceOverwrite = false); + bool extractExteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite); + bool extractInteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite); void saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName); void saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, int segmentsX, int segmentsY); From 39aafcdc2d1e9ed1da424e5744b24d07dda51727 Mon Sep 17 00:00:00 2001 From: Diject Date: Sun, 28 Dec 2025 19:51:17 +0300 Subject: [PATCH 10/23] Implement asynchronous local map extraction and status API --- apps/openmw/engine.cpp | 22 +- apps/openmw/mapextractor.cpp | 385 +++++++++++++++++-------- apps/openmw/mapextractor.hpp | 25 ++ apps/openmw/mwbase/world.hpp | 3 + apps/openmw/mwgui/mapwindow.cpp | 8 +- apps/openmw/mwgui/windowmanagerimp.cpp | 2 +- apps/openmw/mwlua/worldbindings.cpp | 5 + apps/openmw/mwrender/localmap.cpp | 82 ++++-- apps/openmw/mwrender/localmap.hpp | 35 +++ apps/openmw/mwworld/worldimp.cpp | 41 ++- apps/openmw/mwworld/worldimp.hpp | 12 +- files/lua_api/openmw/world.lua | 14 + 12 files changed, 480 insertions(+), 154 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 0d55bedcab..54189f8a1a 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -996,6 +996,7 @@ void OMW::Engine::go() // Map extractor + std::unique_ptr mapExtractor; if (mExtractMaps) { Log(Debug::Info) << "Starting map extraction mode..."; @@ -1004,12 +1005,11 @@ void OMW::Engine::go() Log(Debug::Info) << "Starting map extraction..."; - MapExtractor extractor(*mWorld, mViewer.get(), mWindowManager.get(), mWorldMapOutput, mLocalMapOutput); - extractor.extractWorldMap(); - //extractor.extractLocalMaps(); + //mapExtractor = std::make_unique(*mWorld, mViewer.get(), mWindowManager.get(), mWorldMapOutput, mLocalMapOutput); + //mapExtractor->extractWorldMap(); + //mapExtractor->extractLocalMaps(false); - Log(Debug::Info) << "Map extraction complete. Exiting..."; - //return; + Log(Debug::Info) << "Local map extraction started, will complete during gameplay..."; } @@ -1062,6 +1062,18 @@ void OMW::Engine::go() std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } + + // Update map extraction if active + if (mapExtractor) + { + mapExtractor->update(); + if (mapExtractor->isExtractionComplete()) + { + Log(Debug::Info) << "Map extraction complete."; + mapExtractor.reset(); + } + } + timeManager.updateIsPaused(); if (!timeManager.isPaused()) { diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index e3261e9af4..31d8ba33fb 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -223,9 +223,26 @@ namespace OMW if (!mLocalMap) { Log(Debug::Error) << "Local map not initialized - cannot extract local maps"; + Log(Debug::Error) << "Make sure the game is fully loaded before calling extractLocalMaps"; return; } + Log(Debug::Info) << "LocalMap instance is available, starting extraction"; + + mForceOverwrite = forceOverwrite; + mFramesToWait = 10; // Wait 10 frames before checking (increased from 3) + + startExtraction(forceOverwrite); + } + + void MapExtractor::startExtraction(bool forceOverwrite) + { + // Enable extraction mode to prevent automatic camera cleanup + if (mLocalMap) + { + mLocalMap->setExtractionMode(true); + } + // Get currently active cells MWWorld::Scene* scene = &mWorld.getWorldScene(); if (!scene) @@ -237,34 +254,219 @@ namespace OMW const auto& activeCells = scene->getActiveCells(); Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells..."; - int exteriorCount = 0; - int interiorCount = 0; - int skipped = 0; - + mPendingExtractions.clear(); + for (const MWWorld::CellStore* cellStore : activeCells) { if (cellStore->getCell()->isExterior()) { - if (extractExteriorCell(cellStore, forceOverwrite)) - exteriorCount++; - else - skipped++; + int x = cellStore->getCell()->getGridX(); + int y = cellStore->getCell()->getGridY(); + + std::ostringstream filename; + filename << "(" << x << "," << y << ").png"; + std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); + + if (!forceOverwrite && std::filesystem::exists(outputPath)) + { + Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; + continue; + } + + PendingExtraction extraction; + extraction.cellStore = cellStore; + extraction.isExterior = true; + extraction.outputPath = outputPath; + extraction.framesWaited = 0; + mPendingExtractions.push_back(extraction); } else { - if (extractInteriorCell(cellStore, forceOverwrite)) - interiorCount++; - else - skipped++; + ESM::RefId cellId = cellStore->getCell()->getId(); + std::string cellName(cellStore->getCell()->getNameId()); + + std::string lowerCaseId = cellId.toDebugString(); + std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); + + const std::string invalidChars = "/\\:*?\"<>|"; + lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), + [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), + lowerCaseId.end() + ); + + if (lowerCaseId.empty()) + { + lowerCaseId = "_unnamed_cell_"; + } + + std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); + std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); + + if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) + { + Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; + continue; + } + + PendingExtraction extraction; + extraction.cellStore = cellStore; + extraction.isExterior = false; + extraction.outputPath = texturePath; + extraction.framesWaited = 0; + mPendingExtractions.push_back(extraction); } } - - Log(Debug::Info) << "Saved " << exteriorCount << " exterior local map textures"; - Log(Debug::Info) << "Saved " << interiorCount << " interior local map textures"; - if (skipped > 0) - Log(Debug::Info) << "Skipped " << skipped << " cells (files already exist or without valid textures)"; - - Log(Debug::Info) << "Extraction of active local maps complete"; + + if (!mPendingExtractions.empty()) + { + Log(Debug::Info) << "Queued " << mPendingExtractions.size() << " cells for extraction"; + processNextCell(); + } + else + { + Log(Debug::Info) << "No cells to extract"; + } + } + + void MapExtractor::update() + { + if (mPendingExtractions.empty()) + return; + + PendingExtraction& current = mPendingExtractions.front(); + current.framesWaited++; + + // Wait for the required number of frames before checking + if (current.framesWaited >= mFramesToWait) + { + // Check if the texture is ready before trying to save + bool textureReady = false; + + if (current.isExterior) + { + int x = current.cellStore->getCell()->getGridX(); + int y = current.cellStore->getCell()->getGridY(); + + // Check if the rendered image is ready + osg::ref_ptr image = mLocalMap->getMapImage(x, y); + + if (image && image->s() > 0 && image->t() > 0 && image->data() != nullptr) + { + textureReady = true; + } + else if (current.framesWaited <= 120) + { + return; + } + } + else + { + // For interior cells, check if at least one segment has a valid rendered image + MyGUI::IntRect grid = mLocalMap->getInteriorGrid(); + for (int x = grid.left; x < grid.right && !textureReady; ++x) + { + for (int y = grid.top; y < grid.bottom && !textureReady; ++y) + { + osg::ref_ptr image = mLocalMap->getMapImage(x, y); + if (image && image->s() > 0 && image->t() > 0 && image->data() != nullptr) + { + textureReady = true; + } + } + } + + + if (!textureReady && current.framesWaited <= 120) + { + return; + } + } + + if (textureReady) + { + savePendingExtraction(current); + + mPendingExtractions.erase(mPendingExtractions.begin()); + + if (!mPendingExtractions.empty()) + { + processNextCell(); + } + else + { + // Clean up cameras only after ALL extractions are complete + mLocalMap->cleanupCameras(); + // Disable extraction mode + mLocalMap->setExtractionMode(false); + Log(Debug::Info) << "Extraction of active local maps complete"; + } + } + else if (current.framesWaited > 120) + { + // If we've waited too long (120 frames = ~2 seconds at 60 fps), skip this cell + if (current.isExterior) + { + int x = current.cellStore->getCell()->getGridX(); + int y = current.cellStore->getCell()->getGridY(); + Log(Debug::Warning) << "Timeout waiting for texture for cell (" << x << "," << y << "), skipping"; + } + else + { + std::string cellName(current.cellStore->getCell()->getNameId()); + Log(Debug::Warning) << "Timeout waiting for texture for interior cell: " << cellName << ", skipping"; + } + + mPendingExtractions.erase(mPendingExtractions.begin()); + + if (!mPendingExtractions.empty()) + { + processNextCell(); + } + else + { + // Clean up cameras even if we timed out + mLocalMap->cleanupCameras(); + // Disable extraction mode + mLocalMap->setExtractionMode(false); + Log(Debug::Info) << "Extraction of active local maps complete"; + } + } + // Otherwise keep waiting for the texture to be ready + } + } + + bool MapExtractor::isExtractionComplete() const + { + return mPendingExtractions.empty(); + } + + void MapExtractor::processNextCell() + { + if (mPendingExtractions.empty()) + return; + + const PendingExtraction& extraction = mPendingExtractions.front(); + + if (extraction.isExterior) + { + int x = extraction.cellStore->getCell()->getGridX(); + int y = extraction.cellStore->getCell()->getGridY(); + mLocalMap->clearCellCache(x, y); + } + + mLocalMap->requestMap(const_cast(extraction.cellStore)); + } + + bool MapExtractor::savePendingExtraction(const PendingExtraction& extraction) + { + if (extraction.isExterior) + { + return extractExteriorCell(extraction.cellStore, mForceOverwrite); + } + else + { + return extractInteriorCell(extraction.cellStore, mForceOverwrite); + } } bool MapExtractor::extractExteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite) @@ -272,36 +474,11 @@ namespace OMW int x = cellStore->getCell()->getGridX(); int y = cellStore->getCell()->getGridY(); - // Check if file already exists std::ostringstream filename; filename << "(" << x << "," << y << ").png"; std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); - if (!forceOverwrite && std::filesystem::exists(outputPath)) - { - Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; - return false; - } - - Log(Debug::Info) << "Processing active cell (" << x << "," << y << ")"; - - // Request map generation for this cell - Log(Debug::Verbose) << "Requesting map for cell (" << x << "," << y << ")"; - mLocalMap->requestMap(const_cast(cellStore)); - Log(Debug::Verbose) << "Map requested for cell (" << x << "," << y << ")"; - - // Now try to get the texture - Log(Debug::Verbose) << "Getting texture for cell (" << x << "," << y << ")"; - osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); - - if (!texture) - { - Log(Debug::Warning) << "No texture for cell (" << x << "," << y << ")"; - return false; - } - - // Get the image from the texture (should be set by LocalMapRenderToTexture) - osg::Image* image = texture->getImage(); + osg::ref_ptr image = mLocalMap->getMapImage(x, y); if (!image) { @@ -311,13 +488,11 @@ namespace OMW if (image->s() == 0 || image->t() == 0) { - Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ")"; + Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ") - size: " + << image->s() << "x" << image->t(); return false; } - Log(Debug::Info) << "Got image size: " << image->s() << "x" << image->t() - << " for cell (" << x << "," << y << ")"; - osg::ref_ptr outputImage = new osg::Image(*image, osg::CopyOp::DEEP_COPY_ALL); if (outputImage->s() != 256 || outputImage->t() != 256) @@ -327,15 +502,14 @@ namespace OMW outputImage->scaleImage(256, 256, 1); outputImage = resized; } - if (osgDB::writeImageFile(*outputImage, outputPath.string())) { - Log(Debug::Info) << "Saved local map texture for cell (" << x << "," << y << ")"; + Log(Debug::Info) << "Successfully saved local map for cell (" << x << "," << y << ") to " << outputPath; return true; } else { - Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ")"; + Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ") to " << outputPath; return false; } } @@ -345,32 +519,7 @@ namespace OMW ESM::RefId cellId = cellStore->getCell()->getId(); std::string cellName(cellStore->getCell()->getNameId()); - // Prepare lowercase ID for file naming - std::string lowerCaseId = cellId.toDebugString(); - std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); - - const std::string invalidChars = "/\\:*?\"<>|"; - lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), - [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), - lowerCaseId.end() - ); - - if (lowerCaseId.empty()) - { - lowerCaseId = "_unnamed_cell_"; - } - - // Check if both files exist - std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); - std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); - - if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) - { - Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; - return false; - } - - Log(Debug::Info) << "Processing active interior cell: " << cellName; + Log(Debug::Info) << "Saving interior cell: " << cellName; saveInteriorCellTextures(cellId, cellName); return true; @@ -394,23 +543,23 @@ namespace OMW lowerCaseId = "_unnamed_cell_"; } - int minX = grid.right; int maxX = grid.left; int minY = grid.bottom; int maxY = grid.top; - // Cache textures and find bounds - std::map, osg::ref_ptr> textureCache; + // Cache images and find bounds + std::map, osg::ref_ptr> imageCache; for (int x = grid.left; x < grid.right; ++x) { for (int y = grid.top; y < grid.bottom; ++y) { - osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); - if (texture && texture->getImage()) + // Get the rendered image directly from camera attachment + osg::ref_ptr image = mLocalMap->getMapImage(x, y); + if (image && image->s() > 0 && image->t() > 0 && image->data() != nullptr) { - textureCache[{x, y}] = texture; + imageCache[{x, y}] = image; minX = std::min(minX, x); maxX = std::max(maxX, x); minY = std::min(minY, y); @@ -420,13 +569,19 @@ namespace OMW } if (minX > maxX || minY > maxY) + { + Log(Debug::Warning) << "No valid image segments found for interior cell: " << cellName; return; + } int segmentsX = maxX - minX + 1; int segmentsY = maxY - minY + 1; if (segmentsX <= 0 || segmentsY <= 0) + { + Log(Debug::Warning) << "Invalid segment dimensions for interior cell: " << cellName; return; + } int totalWidth = segmentsX * 256; int totalHeight = segmentsY * 256; @@ -441,12 +596,11 @@ namespace OMW { for (int y = minY; y <= maxY; ++y) { - auto it = textureCache.find({x, y}); - if (it == textureCache.end()) + auto it = imageCache.find({x, y}); + if (it == imageCache.end()) continue; - osg::ref_ptr texture = it->second; - osg::Image* segmentImage = texture->getImage(); + osg::ref_ptr segmentImage = it->second; int segWidth = segmentImage->s(); int segHeight = segmentImage->t(); @@ -474,7 +628,16 @@ namespace OMW } std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); - osgDB::writeImageFile(*combinedImage, texturePath.string()); + + if (osgDB::writeImageFile(*combinedImage, texturePath.string())) + { + Log(Debug::Info) << "Successfully saved interior map to " << texturePath; + } + else + { + Log(Debug::Error) << "Failed to write interior map to " << texturePath; + return; + } saveInteriorMapInfo(cellId, lowerCaseId, segmentsX, segmentsY); } @@ -486,37 +649,27 @@ namespace OMW if (!cell) return; - float nA = 0.0f; - float mX = 0.0f; - float mY = 0.0f; - - MWWorld::ConstPtr northmarker = cell->searchConst(ESM::RefId::stringRefId("northmarker")); - if (!northmarker.isEmpty()) - { - osg::Quat orient(-northmarker.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, 1)); - osg::Vec3f dir = orient * osg::Vec3f(0, 1, 0); - nA = std::atan2(dir.x(), dir.y()); - } - - osg::BoundingBox bounds; - osg::ComputeBoundsVisitor computeBoundsVisitor; - computeBoundsVisitor.setTraversalMask(MWRender::Mask_Scene | MWRender::Mask_Terrain | - MWRender::Mask_Object | MWRender::Mask_Static); + // Get the bounds, center and angle that LocalMap actually used for rendering + const osg::BoundingBox& bounds = mLocalMap->getInteriorBounds(); + const osg::Vec2f& center = mLocalMap->getInteriorCenter(); + const float nA = mLocalMap->getInteriorAngle(); - MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); - if (renderingManager && renderingManager->getLightRoot()) - { - renderingManager->getLightRoot()->accept(computeBoundsVisitor); - bounds = computeBoundsVisitor.getBoundingBox(); - } - - osg::Vec2f center(bounds.center().x(), bounds.center().y()); osg::Vec2f min(bounds.xMin(), bounds.yMin()); const float mapWorldSize = Constants::CellSizeInUnits; - mX = ((0 - center.x()) * std::cos(nA) - (0 - center.y()) * std::sin(nA) + center.x() - min.x()) / mapWorldSize * 256.0f * 2.0f; - mY = ((0 - center.x()) * std::sin(nA) + (0 - center.y()) * std::cos(nA) + center.y() - min.y()) / mapWorldSize * 256.0f * 2.0f; + // Calculate position of world origin (0,0) on the rotated map + osg::Vec2f toOrigin(0.0f - center.x(), 0.0f - center.y()); + float rotatedX = toOrigin.x() * std::cos(nA) - toOrigin.y() * std::sin(nA) + center.x(); + float rotatedY = toOrigin.x() * std::sin(nA) + toOrigin.y() * std::cos(nA) + center.y(); + + // Convert to texture coordinates (pixels from bottom-left corner) + float oX = (rotatedX - min.x()) / mapWorldSize * 256.0f; + float oY = (rotatedY - min.y()) / mapWorldSize * 256.0f; + + float totalHeight = segmentsY * 256.0f; + float mX = oX * 2.0f; + float mY = (oY - totalHeight) * 2.0f; std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); std::ofstream file(yamlPath); @@ -530,6 +683,8 @@ namespace OMW file << "nA: " << nA << "\n"; file << "mX: " << mX << "\n"; file << "mY: " << mY << "\n"; + file << "oX: " << oX << "\n"; + file << "oY: " << oY << "\n"; file << "width: " << segmentsX << "\n"; file << "height: " << segmentsY << "\n"; diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 800a08c16a..2989aced90 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include @@ -50,8 +52,23 @@ namespace OMW void extractWorldMap(); void extractLocalMaps(bool forceOverwrite = false); + + // Called every frame to process pending extractions + void update(); + + // Check if extraction is complete + bool isExtractionComplete() const; private: + struct PendingExtraction + { + const MWWorld::CellStore* cellStore; + bool isExterior; + std::filesystem::path outputPath; + int framesWaited; + std::function completionCallback; + }; + MWWorld::World& mWorld; osgViewer::Viewer* mViewer; MWBase::WindowManager* mWindowManager; @@ -60,10 +77,18 @@ namespace OMW std::unique_ptr mGlobalMap; MWRender::LocalMap* mLocalMap; + + std::vector mPendingExtractions; + int mFramesToWait; + bool mForceOverwrite; void saveWorldMapTexture(); void saveWorldMapInfo(); + void startExtraction(bool forceOverwrite); + void processNextCell(); + bool savePendingExtraction(const PendingExtraction& extraction); + bool extractExteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite); bool extractInteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite); void saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName); diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 969be697a4..9a814fb47a 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -618,6 +618,9 @@ namespace MWBase virtual void extractLocalMaps(const std::string& localMapOutput) = 0; ///< Extract local maps to the specified directory + + virtual bool isMapExtractionActive() const = 0; + ///< Check if map extraction is currently in progress }; } diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 637cb7e343..0f5c83be2b 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -1159,7 +1159,9 @@ namespace MWGui void MapWindow::cellExplored(int x, int y) { - mGlobalMapRender->cleanupCameras(); + // Note: Don't cleanup cameras here! This is called frequently during gameplay + // and would interfere with map extraction which batches multiple camera renders + // mGlobalMapRender->cleanupCameras(); mGlobalMapRender->exploreCell(x, y, mLocalMapRender->getMapTexture(x, y)); } @@ -1167,6 +1169,10 @@ namespace MWGui { LocalMapBase::onFrame(dt); NoDrop::onFrame(dt); + + // Note: Don't cleanup cameras here during normal gameplay + // Cameras are cleaned up only when explicitly requested (e.g., after map extraction) + // For global map overlay updates, cleanup happens in cellExplored() when needed } void MapWindow::setGlobalMapMarkerTooltip(MyGUI::Widget* markerWidget, int x, int y) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index e53c0a5b39..a4e9b26fa5 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1041,7 +1041,7 @@ namespace MWGui mToolTips->onFrame(frameDuration); if (mLocalMapRender) - mLocalMapRender->cleanupCameras(); + //mLocalMapRender->cleanupCameras(); mDebugWindow->onFrame(frameDuration); diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index 234c0cac76..dbd842b6f7 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -298,6 +298,11 @@ namespace MWLua "disableExtractionModeAction"); }; + api["isMapExtractionActive"] = [lua = context.mLua]() -> bool { + checkGameInitialized(lua); + return MWBase::Environment::get().getWorld()->isMapExtractionActive(); + }; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 3546c29852..28abe98c4d 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -169,14 +169,15 @@ namespace MWRender void LocalMap::setupRenderToTexture( int segmentX, int segmentY, float left, float top, const osg::Vec3d& upVector, float zmin, float zmax) { - mLocalMapRTTs.emplace_back( - new LocalMapRenderToTexture(mSceneRoot, mMapResolution, mMapWorldSize, left, top, upVector, zmin, zmax)); + auto rttNode = new LocalMapRenderToTexture(mSceneRoot, mMapResolution, mMapWorldSize, left, top, upVector, zmin, zmax); + mLocalMapRTTs.emplace_back(rttNode); mRoot->addChild(mLocalMapRTTs.back()); MapSegment& segment = mInterior ? mInteriorSegments[std::make_pair(segmentX, segmentY)] : mExteriorSegments[std::make_pair(segmentX, segmentY)]; segment.mMapTexture = static_cast(mLocalMapRTTs.back()->getColorTexture(nullptr)); + segment.mRTT = rttNode; // Store reference to RTT node } void LocalMap::requestMap(const MWWorld::CellStore* cell) @@ -239,6 +240,36 @@ namespace MWRender return found->second.mFogOfWarTexture; } + osg::ref_ptr LocalMap::getMapImage(int x, int y) + { + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) + return osg::ref_ptr(); + + MapSegment& segment = found->second; + + if (!segment.mRTT) + return osg::ref_ptr(); + + osg::Camera* camera = segment.mRTT->getCamera(nullptr); + if (!camera) + return osg::ref_ptr(); + + const osg::Camera::BufferAttachmentMap& attachments = camera->getBufferAttachmentMap(); + auto it = attachments.find(osg::Camera::COLOR_BUFFER); + if (it != attachments.end()) + { + osg::Image* img = it->second._image.get(); + if (img && img->s() > 0 && img->t() > 0 && img->data() != nullptr) + { + return img; + } + } + + return osg::ref_ptr(); + } + void LocalMap::cleanupCameras() { auto it = mLocalMapRTTs.begin(); @@ -475,6 +506,21 @@ namespace MWRender return mRoot; } + void LocalMap::setExtractionMode(bool enabled) + { + mExtractionMode = enabled; + } + + void LocalMap::clearCellCache(int x, int y) + { + auto it = mExteriorSegments.find(std::make_pair(x, y)); + if (it != mExteriorSegments.end()) + { + // Reset the render flags to force re-rendering + it->second.mLastRenderNeighbourFlags = 0; + } + } + void LocalMap::updatePlayer(const osg::Vec3f& position, const osg::Quat& orientation, float& u, float& v, int& x, int& y, osg::Vec3f& direction) { @@ -775,28 +821,20 @@ namespace MWRender camera->addChild(lightSource); camera->addChild(mSceneRoot); - // CRITICAL FIX: Setup both texture and image for CPU-side access (needed by mapextractor) - // First attach texture for normal rendering (GPU-side) - osg::ref_ptr texture = new osg::Texture2D(); - texture->setTextureSize(camera->getViewport()->width(), camera->getViewport()->height()); - texture->setInternalFormat(GL_RGB); - texture->setSourceFormat(GL_RGB); - texture->setSourceType(GL_UNSIGNED_BYTE); - texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - camera->attach(osg::Camera::COLOR_BUFFER, texture.get()); + // CRITICAL: Attach an Image to COLOR_BUFFER to enable pixel readback from FBO + // This is required for map extraction functionality + // The image MUST be pre-allocated before attaching to the camera + osg::ref_ptr image = new osg::Image; - // Then attach Image for CPU-side readback - // OSG will automatically copy rendered pixels to this Image - osg::ref_ptr image = new osg::Image(); - image->setPixelFormat(GL_RGB); - image->setDataType(GL_UNSIGNED_BYTE); - camera->attach(osg::Camera::COLOR_BUFFER, image.get()); + // Get the texture size from the camera's viewport + const osg::Viewport* vp = camera->getViewport(); + int width = vp ? vp->width() : 512; + int height = vp ? vp->height() : 512; - // Also set the Image on the texture so mapextractor can retrieve it via texture->getImage() - texture->setImage(image.get()); + // Allocate the image with the same format as the color buffer + image->allocateImage(width, height, 1, GL_RGB, GL_UNSIGNED_BYTE); + + camera->attach(osg::Camera::COLOR_BUFFER, image); } void CameraLocalUpdateCallback::operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv) diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index c5b63766b7..f7201bf6df 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -62,6 +62,12 @@ namespace MWRender osg::ref_ptr getMapTexture(int x, int y); osg::ref_ptr getFogOfWarTexture(int x, int y); + + /** + * Get the rendered map image for a cell (for extraction purposes) + * Returns the osg::Image that contains the rendered pixel data + */ + osg::ref_ptr getMapImage(int x, int y); /** * Removes cameras that have already been rendered. Should be called every frame to ensure that @@ -97,9 +103,36 @@ namespace MWRender */ bool isPositionExplored(float nX, float nY, int x, int y); + /** + * Clear the render cache for a specific exterior cell, forcing it to be re-rendered on next request + */ + void clearCellCache(int x, int y); + osg::Group* getRoot(); MyGUI::IntRect getInteriorGrid() const; + + /** + * Enable/disable extraction mode. When enabled, cameras won't be automatically cleaned up + * after rendering, allowing batch extraction of multiple maps. + */ + void setExtractionMode(bool enabled); + bool isExtractionMode() const { return mExtractionMode; } + + /** + * Get interior map bounds (with padding applied) - for map extraction + */ + const osg::BoundingBox& getInteriorBounds() const { return mBounds; } + + /** + * Get interior map center after rotation - for map extraction + */ + const osg::Vec2f& getInteriorCenter() const { return mCenter; } + + /** + * Get interior map rotation angle - for map extraction + */ + float getInteriorAngle() const { return mAngle; } private: osg::ref_ptr mRoot; @@ -132,6 +165,7 @@ namespace MWRender osg::ref_ptr mMapTexture; osg::ref_ptr mFogOfWarTexture; osg::ref_ptr mFogOfWarImage; + osg::ref_ptr mRTT; // Reference to the RTT node for this segment }; typedef std::map, MapSegment> SegmentMap; @@ -160,6 +194,7 @@ namespace MWRender osg::BoundingBox mBounds; osg::Vec2f mCenter; bool mInterior; + bool mExtractionMode = false; std::uint8_t getExteriorNeighbourFlags(int cellX, int cellY) const; }; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index d83e3f9fa2..16a8067d90 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1660,6 +1660,17 @@ namespace MWWorld if (mGoToJail && !paused) goToJail(); + // Update map extraction if active + if (mMapExtractor) + { + mMapExtractor->update(); + if (mMapExtractor->isExtractionComplete()) + { + Log(Debug::Info) << "Map extraction complete."; + mMapExtractor.reset(); + } + } + // Reset "traveling" flag - there was a frame to detect traveling. mPlayerTraveling = false; @@ -3931,14 +3942,20 @@ namespace MWWorld throw std::runtime_error("Viewer is not initialized"); } + // If extraction is already in progress, ignore the request + if (mMapExtractor) + { + Log(Debug::Warning) << "Map extraction is already in progress"; + return; + } + std::string outputPath = worldMapOutput.empty() ? getWorldMapOutputPath() : worldMapOutput; MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - OMW::MapExtractor extractor(*this, viewer, windowManager, outputPath, ""); + mMapExtractor = std::make_unique(*this, viewer, windowManager, outputPath, ""); Log(Debug::Info) << "Starting world map extraction to: " << outputPath; - extractor.extractWorldMap(); - Log(Debug::Info) << "World map extraction complete"; + mMapExtractor->extractWorldMap(); } void World::extractLocalMaps(const std::string& localMapOutput) @@ -3954,13 +3971,25 @@ namespace MWWorld throw std::runtime_error("Viewer is not initialized"); } + // If extraction is already in progress, ignore the request + if (mMapExtractor) + { + Log(Debug::Warning) << "Map extraction is already in progress"; + return; + } + std::string outputPath = localMapOutput.empty() ? getLocalMapOutputPath() : localMapOutput; MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - OMW::MapExtractor extractor(*this, viewer, windowManager, "", outputPath); + mMapExtractor = std::make_unique(*this, viewer, windowManager, "", outputPath); Log(Debug::Info) << "Starting local maps extraction to: " << outputPath; - extractor.extractLocalMaps(); - Log(Debug::Info) << "Local maps extraction complete"; + mMapExtractor->extractLocalMaps(false); + Log(Debug::Info) << "Local maps extraction started, will complete during gameplay..."; + } + + bool World::isMapExtractionActive() const + { + return mMapExtractor != nullptr; } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 71c1ee6203..44989f08bc 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -12,6 +12,8 @@ #include "../mwbase/world.hpp" +#include "../mapextractor.hpp" + #include "contentloader.hpp" #include "esmstore.hpp" #include "globals.hpp" @@ -102,10 +104,11 @@ namespace MWWorld std::unique_ptr mPhysics; std::unique_ptr mNavigator; std::unique_ptr mRendering; - std::unique_ptr mWorldScene; - std::unique_ptr mWeatherManager; - std::unique_ptr mTimeManager; - std::unique_ptr mProjectileManager; + std::unique_ptr mWorldScene; + std::unique_ptr mWeatherManager; + std::unique_ptr mTimeManager; + std::unique_ptr mProjectileManager; + std::unique_ptr mMapExtractor; bool mSky; bool mGodMode; @@ -686,6 +689,7 @@ namespace MWWorld void extractWorldMap(const std::string& worldMapOutput) override; void extractLocalMaps(const std::string& localMapOutput) override; + bool isMapExtractionActive() const override; }; } diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index c89eb3b0d6..bbf27a7ce4 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -253,4 +253,18 @@ -- @function [parent=#world] disableExtractionMode -- @usage world.disableExtractionMode() +--- +-- Check if map extraction is currently in progress. +-- Returns true if world map or local map extraction is active, false otherwise. +-- Use this to avoid starting multiple extractions simultaneously. +-- @function [parent=#world] isMapExtractionActive +-- @return #boolean true if map extraction is active, false otherwise +-- @usage +-- if not world.isMapExtractionActive() then +-- world.extractWorldMap("path/to/output") +-- else +-- print("Map extraction already in progress") +-- end + return nil + From fa5496407baa940bd88b083d76aaf30bac55fabb Mon Sep 17 00:00:00 2001 From: Diject Date: Mon, 29 Dec 2025 14:31:45 +0300 Subject: [PATCH 11/23] Change default options to skip menu and disable sound --- apps/openmw/engine.cpp | 26 ++++++-------------------- apps/openmw/options.cpp | 8 ++++---- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 54189f8a1a..d0dc9915a5 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -364,14 +364,14 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mSelectDepthFormatOperation(new SceneUtil::SelectDepthFormatOperation()) , mSelectColorFormatOperation(new SceneUtil::Color::SelectColorFormatOperation()) , mStereoManager(nullptr) - , mSkipMenu(false) - , mUseSound(true) + , mSkipMenu(true) + , mUseSound(false) , mCompileAll(false) , mCompileAllDialogue(false) , mWarningsMode(1) , mScriptConsoleMode(false) , mActivationDistanceOverride(-1) - , mGrab(true) + , mGrab(false) , mExportFonts(false) , mRandomSeed(0) , mNewGame(false) @@ -483,8 +483,8 @@ void OMW::Engine::addGroundcoverFile(const std::string& file) void OMW::Engine::setSkipMenu(bool skipMenu, bool newGame) { - mSkipMenu = skipMenu; - mNewGame = newGame; + mSkipMenu = true; + mNewGame = false; } void OMW::Engine::createWindow() @@ -997,20 +997,6 @@ void OMW::Engine::go() // Map extractor std::unique_ptr mapExtractor; - if (mExtractMaps) - { - Log(Debug::Info) << "Starting map extraction mode..."; - - mStateManager->newGame(true); - - Log(Debug::Info) << "Starting map extraction..."; - - //mapExtractor = std::make_unique(*mWorld, mViewer.get(), mWindowManager.get(), mWorldMapOutput, mLocalMapOutput); - //mapExtractor->extractWorldMap(); - //mapExtractor->extractLocalMaps(false); - - Log(Debug::Info) << "Local map extraction started, will complete during gameplay..."; - } // Start the game @@ -1069,7 +1055,7 @@ void OMW::Engine::go() mapExtractor->update(); if (mapExtractor->isExtractionComplete()) { - Log(Debug::Info) << "Map extraction complete."; + Log(Debug::Info) << "Extraction process completed."; mapExtractor.reset(); } } diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index df78a682c9..d1f6f9357d 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -44,7 +44,7 @@ namespace OpenMW bpo::value()->default_value(StringsVector(), "")->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon"); - addOption("no-sound", bpo::value()->implicit_value(true)->default_value(false), "disable all sounds"); + addOption("no-sound", bpo::value()->implicit_value(true)->default_value(true), "disable all sounds"); addOption("script-all", bpo::value()->implicit_value(true)->default_value(false), "compile all scripts (excluding dialogue scripts) at startup"); @@ -68,7 +68,7 @@ namespace OpenMW "load a save game file on game startup (specify an absolute filename or a filename relative to the current " "working directory)"); - addOption("skip-menu", bpo::value()->implicit_value(true)->default_value(false), + addOption("skip-menu", bpo::value()->implicit_value(true)->default_value(true), "skip main menu on game startup"); addOption("new-game", bpo::value()->implicit_value(true)->default_value(false), @@ -85,7 +85,7 @@ namespace OpenMW bpo::value()->default_value(Fallback::FallbackMap(), "")->multitoken()->composing(), "fallback values"); - addOption("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor"); + addOption("no-grab", bpo::value()->implicit_value(true)->default_value(true), "Don't grab mouse cursor"); addOption("export-fonts", bpo::value()->implicit_value(true)->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory"); @@ -101,7 +101,7 @@ namespace OpenMW addOption("local-map-output", bpo::value()->default_value(""), "directory to save local map textures (default: textures/advanced_world_map/local)"); - addOption("extract-maps", bpo::value()->implicit_value(true)->default_value(false), + addOption("extract-maps", bpo::value()->implicit_value(true)->default_value(true), "extract world and local map textures and exit"); return desc; From 1936e902efcf3abbfc78316c50e859dda9433401 Mon Sep 17 00:00:00 2001 From: Diject Date: Mon, 29 Dec 2025 14:58:29 +0300 Subject: [PATCH 12/23] Improve camera cleanup in map extraction process --- apps/openmw/mapextractor.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index 31d8ba33fb..744e454492 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -325,6 +325,12 @@ namespace OMW else { Log(Debug::Info) << "No cells to extract"; + // Clean up any cameras that may have been created + if (mLocalMap) + { + mLocalMap->cleanupCameras(); + mLocalMap->setExtractionMode(false); + } } } @@ -388,14 +394,17 @@ namespace OMW mPendingExtractions.erase(mPendingExtractions.begin()); + // Clean up the camera for the current cell immediately after saving + // This prevents memory buildup when processing many cells + if (mLocalMap) + mLocalMap->cleanupCameras(); + if (!mPendingExtractions.empty()) { processNextCell(); } else { - // Clean up cameras only after ALL extractions are complete - mLocalMap->cleanupCameras(); // Disable extraction mode mLocalMap->setExtractionMode(false); Log(Debug::Info) << "Extraction of active local maps complete"; @@ -418,14 +427,16 @@ namespace OMW mPendingExtractions.erase(mPendingExtractions.begin()); + // Clean up cameras even after timeout + if (mLocalMap) + mLocalMap->cleanupCameras(); + if (!mPendingExtractions.empty()) { processNextCell(); } else { - // Clean up cameras even if we timed out - mLocalMap->cleanupCameras(); // Disable extraction mode mLocalMap->setExtractionMode(false); Log(Debug::Info) << "Extraction of active local maps complete"; From 9bdf26eb8932c7330abb144ba6e09903d6ba5809 Mon Sep 17 00:00:00 2001 From: Diject Date: Mon, 29 Dec 2025 21:58:13 +0300 Subject: [PATCH 13/23] Skip omwscripts and groundcover files in content loading --- apps/openmw/main.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 9badb4f31a..91da053dcd 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -123,14 +123,20 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati for (auto& file : content) { + if (file.ends_with(".omwscripts")) + { + Log(Debug::Warning) << "Skipping omwscripts file in content list: " << file; + continue; + } engine.addContentFile(file); } - StringsVector groundcover = variables["groundcover"].as(); - for (auto& file : groundcover) - { - engine.addGroundcoverFile(file); - } + Log(Debug::Warning) << "Skipping groundcover files."; + //StringsVector groundcover = variables["groundcover"].as(); + //for (auto& file : groundcover) + //{ + // engine.addGroundcoverFile(file); + //} if (variables.count("lua-scripts")) { From 2558cc55a3623d6c9711937770f5034325552409 Mon Sep 17 00:00:00 2001 From: Diject Date: Tue, 30 Dec 2025 12:08:35 +0300 Subject: [PATCH 14/23] add CMakeSettings.json for x64-Debug configuration --- CMakeSettings.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 CMakeSettings.json diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 0000000000..9204f06ebd --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + } + ] +} \ No newline at end of file From a2b80f0a23b5c6f8d90a44a1fe5ebaee6b4b35c6 Mon Sep 17 00:00:00 2001 From: Diject Date: Tue, 30 Dec 2025 13:41:07 +0300 Subject: [PATCH 15/23] fix camera cleanup --- apps/openmw/mwgui/windowmanagerimp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index a4e9b26fa5..a1f63dd9d8 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1040,8 +1040,8 @@ namespace MWGui mToolTips->onFrame(frameDuration); - if (mLocalMapRender) - //mLocalMapRender->cleanupCameras(); + //if (mLocalMapRender) + // mLocalMapRender->cleanupCameras(); mDebugWindow->onFrame(frameDuration); From d27a9143c888c6e11c45241ed380ae0960ba500c Mon Sep 17 00:00:00 2001 From: Diject Date: Tue, 30 Dec 2025 15:54:57 +0300 Subject: [PATCH 16/23] Suppress NaN warnings in OSG log handler --- apps/openmw/main.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 91da053dcd..a4e73672eb 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -186,13 +186,28 @@ namespace { class OSGLogHandler : public osg::NotifyHandler { + int ignoreNext = 0; + void notify(osg::NotifySeverity severity, const char* msg) override { + if (ignoreNext > 0) + { + --ignoreNext; + return; + } + // Copy, because osg logging is not thread safe. std::string msgCopy(msg); if (msgCopy.empty()) return; + // Ignore because I don't know how to fix it + if (msgCopy.find("CullVisitor::apply(Geode&) detected NaN") != std::string::npos) + { + ignoreNext = 8; + return; + } + Debug::Level level; switch (severity) { From e8d844e7e55bdd4b58d2c1cabbf50ce56080d120 Mon Sep 17 00:00:00 2001 From: Diject Date: Tue, 30 Dec 2025 18:49:08 +0300 Subject: [PATCH 17/23] Refactors MapExtractor to use engine options --- apps/openmw/engine.cpp | 9 ++-- apps/openmw/engine.hpp | 4 +- apps/openmw/main.cpp | 17 ++++-- apps/openmw/mapextractor.cpp | 58 ++++++-------------- apps/openmw/mapextractor.hpp | 17 +++--- apps/openmw/mwbase/world.hpp | 8 +-- apps/openmw/mwlua/worldbindings.cpp | 14 +++-- apps/openmw/mwworld/worldimp.cpp | 83 +++++++---------------------- apps/openmw/mwworld/worldimp.hpp | 14 +++-- apps/openmw/options.cpp | 4 +- files/lua_api/openmw/world.lua | 19 ++++--- 11 files changed, 97 insertions(+), 150 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index d0dc9915a5..2dec1d478e 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -375,9 +375,9 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mExportFonts(false) , mRandomSeed(0) , mNewGame(false) - , mExtractMaps(false) , mCfgMgr(configurationManager) , mGlMaxTextureImageUnits(0) + , mOverwriteMaps(false) { #if SDL_VERSION_ATLEAST(2, 24, 0) SDL_SetHint(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, "1"); @@ -836,7 +836,8 @@ void OMW::Engine::prepareEngine() // Create the world mWorld = std::make_unique( - mResourceSystem.get(), mActivationDistanceOverride, mCellName, mCfgMgr.getUserDataPath()); + mResourceSystem.get(), mActivationDistanceOverride, mCellName, mCfgMgr.getUserDataPath(), + mWorldMapOutput, mLocalMapOutput, mOverwriteMaps); mEnvironment.setWorld(*mWorld); mEnvironment.setWorldModel(mWorld->getWorldModel()); mEnvironment.setESMStore(mWorld->getStore()); @@ -1158,7 +1159,7 @@ void OMW::Engine::setLocalMapOutput(const std::string& path) mLocalMapOutput = path; } -void OMW::Engine::setExtractMaps(bool extract) +void OMW::Engine::setOverwriteMaps(bool overwrite) { - mExtractMaps = extract; + mOverwriteMaps = overwrite; } diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 11be9eac62..89ab520cba 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -185,7 +185,7 @@ namespace OMW std::string mWorldMapOutput; std::string mLocalMapOutput; - bool mExtractMaps; + bool mOverwriteMaps; Files::ConfigurationManager& mCfgMgr; int mGlMaxTextureImageUnits; @@ -272,6 +272,8 @@ namespace OMW void setLocalMapOutput(const std::string& path); + void setOverwriteMaps(bool overwrite); + void setExtractMaps(bool extract); void setRecastMaxLogLevel(Debug::Level value) { mMaxRecastLogLevel = value; } diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index a4e73672eb..1f0d7c2e56 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -167,17 +167,26 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati std::string worldMapOutput = variables["world-map-output"].as(); std::string localMapOutput = variables["local-map-output"].as(); - bool extractMaps = variables["extract-maps"].as(); - if (worldMapOutput.empty() && extractMaps) + auto removeQuotes = [](std::string& str) { + if (str.size() >= 2 && ((str.front() == '"' && str.back() == '"') || (str.front() == '\'' && str.back() == '\''))) + { + str = str.substr(1, str.size() - 2); + } + }; + + removeQuotes(worldMapOutput); + removeQuotes(localMapOutput); + + if (worldMapOutput.empty()) worldMapOutput = Files::pathToUnicodeString(std::filesystem::current_path() / "textures" / "advanced_world_map" / "custom"); - if (localMapOutput.empty() && extractMaps) + if (localMapOutput.empty()) localMapOutput = Files::pathToUnicodeString(std::filesystem::current_path() / "textures" / "advanced_world_map" / "local"); engine.setWorldMapOutput(worldMapOutput); engine.setLocalMapOutput(localMapOutput); - engine.setExtractMaps(extractMaps); + engine.setOverwriteMaps(variables["overwrite-maps"].as()); return true; } diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index 744e454492..86578ee1eb 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -36,14 +36,14 @@ namespace OMW { - MapExtractor::MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, - MWBase::WindowManager* windowManager, const std::string& worldMapOutput, const std::string& localMapOutput) - : mWorld(world) - , mViewer(viewer) - , mWindowManager(windowManager) - , mWorldMapOutputDir(worldMapOutput) + MapExtractor::MapExtractor(const std::string& worldMapOutput, const std::string& localMapOutput, + bool forceOverwrite, MWRender::RenderingManager* renderingManager, const MWWorld::ESMStore* store) + : mWorldMapOutputDir(worldMapOutput) , mLocalMapOutputDir(localMapOutput) + , mRenderingManager(renderingManager) + , mStore(store) , mLocalMap(nullptr) + , mForceOverwrite(forceOverwrite) { // Only create directories if paths are not empty if (!mWorldMapOutputDir.empty()) @@ -52,14 +52,13 @@ namespace OMW std::filesystem::create_directories(mLocalMapOutputDir); // Create GlobalMap instance - MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); - if (!renderingManager) + if (!mRenderingManager) { Log(Debug::Error) << "RenderingManager is null in MapExtractor constructor"; throw std::runtime_error("RenderingManager is null"); } - osg::Group* lightRoot = renderingManager->getLightRoot(); + osg::Group* lightRoot = mRenderingManager->getLightRoot(); if (!lightRoot) { Log(Debug::Error) << "LightRoot is null in MapExtractor constructor"; @@ -73,7 +72,7 @@ namespace OMW throw std::runtime_error("Root node is null"); } - SceneUtil::WorkQueue* workQueue = renderingManager->getWorkQueue(); + SceneUtil::WorkQueue* workQueue = mRenderingManager->getWorkQueue(); if (!workQueue) { Log(Debug::Error) << "WorkQueue is null in MapExtractor constructor"; @@ -83,8 +82,7 @@ namespace OMW try { mGlobalMap = std::make_unique(root, workQueue); - // Get LocalMap from WindowManager - it will be set after initUI is called - // For now just set to nullptr + // LocalMap will be set later via setLocalMap() method mLocalMap = nullptr; } catch (const std::exception& e) @@ -157,14 +155,13 @@ namespace OMW int width = mGlobalMap->getWidth(); int height = mGlobalMap->getHeight(); - const MWWorld::ESMStore& store = mWorld.getStore(); int minX = std::numeric_limits::max(); int maxX = std::numeric_limits::min(); int minY = std::numeric_limits::max(); int maxY = std::numeric_limits::min(); - MWWorld::Store::iterator it = store.get().extBegin(); - for (; it != store.get().extEnd(); ++it) + MWWorld::Store::iterator it = mStore->get().extBegin(); + for (; it != mStore->get().extEnd(); ++it) { if (it->getGridX() < minX) minX = it->getGridX(); @@ -201,7 +198,7 @@ namespace OMW Log(Debug::Info) << "Saved world map info: " << infoPath; } - void MapExtractor::extractLocalMaps(bool forceOverwrite) + void MapExtractor::extractLocalMaps(const std::vector& activeCells) { Log(Debug::Info) << "Extracting active local maps..."; @@ -214,28 +211,20 @@ namespace OMW // Create output directory if it doesn't exist std::filesystem::create_directories(mLocalMapOutputDir); - // Get LocalMap from WindowManager now that UI is initialized - if (mWindowManager) - { - mLocalMap = mWindowManager->getLocalMapRender(); - } - if (!mLocalMap) { Log(Debug::Error) << "Local map not initialized - cannot extract local maps"; - Log(Debug::Error) << "Make sure the game is fully loaded before calling extractLocalMaps"; return; } Log(Debug::Info) << "LocalMap instance is available, starting extraction"; - mForceOverwrite = forceOverwrite; mFramesToWait = 10; // Wait 10 frames before checking (increased from 3) - startExtraction(forceOverwrite); + startExtraction(activeCells); } - void MapExtractor::startExtraction(bool forceOverwrite) + void MapExtractor::startExtraction(const std::vector& activeCells) { // Enable extraction mode to prevent automatic camera cleanup if (mLocalMap) @@ -243,15 +232,6 @@ namespace OMW mLocalMap->setExtractionMode(true); } - // Get currently active cells - MWWorld::Scene* scene = &mWorld.getWorldScene(); - if (!scene) - { - Log(Debug::Error) << "Scene not available"; - throw std::runtime_error("Scene not available"); - } - - const auto& activeCells = scene->getActiveCells(); Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells..."; mPendingExtractions.clear(); @@ -267,7 +247,7 @@ namespace OMW filename << "(" << x << "," << y << ").png"; std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); - if (!forceOverwrite && std::filesystem::exists(outputPath)) + if (!mForceOverwrite && std::filesystem::exists(outputPath)) { Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; continue; @@ -302,7 +282,7 @@ namespace OMW std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); - if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) + if (!mForceOverwrite && std::filesystem::exists(yamlPath)) { Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; continue; @@ -656,10 +636,6 @@ namespace OMW void MapExtractor::saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, int segmentsX, int segmentsY) { - MWWorld::CellStore* cell = mWorld.getWorldModel().findCell(cellId); - if (!cell) - return; - // Get the bounds, center and angle that LocalMap actually used for rendering const osg::BoundingBox& bounds = mLocalMap->getInteriorBounds(); const osg::Vec2f& center = mLocalMap->getInteriorCenter(); diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 2989aced90..9be1fcbfc4 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -28,12 +28,14 @@ namespace MWRender { class GlobalMap; class LocalMap; + class RenderingManager; } namespace MWWorld { class World; class CellStore; + class ESMStore; } namespace MWBase @@ -46,12 +48,14 @@ namespace OMW class MapExtractor { public: - MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, MWBase::WindowManager* windowManager, - const std::string& worldMapOutput, const std::string& localMapOutput); + MapExtractor(const std::string& worldMapOutput, const std::string& localMapOutput, bool forceOverwrite, + MWRender::RenderingManager* renderingManager, const MWWorld::ESMStore* store); ~MapExtractor(); + void setLocalMap(MWRender::LocalMap* localMap) { mLocalMap = localMap; } + void extractWorldMap(); - void extractLocalMaps(bool forceOverwrite = false); + void extractLocalMaps(const std::vector& activeCells); // Called every frame to process pending extractions void update(); @@ -69,11 +73,10 @@ namespace OMW std::function completionCallback; }; - MWWorld::World& mWorld; - osgViewer::Viewer* mViewer; - MWBase::WindowManager* mWindowManager; std::filesystem::path mWorldMapOutputDir; std::filesystem::path mLocalMapOutputDir; + MWRender::RenderingManager* mRenderingManager; + const MWWorld::ESMStore* mStore; std::unique_ptr mGlobalMap; MWRender::LocalMap* mLocalMap; @@ -85,7 +88,7 @@ namespace OMW void saveWorldMapTexture(); void saveWorldMapInfo(); - void startExtraction(bool forceOverwrite); + void startExtraction(const std::vector& activeCells); void processNextCell(); bool savePendingExtraction(const PendingExtraction& extraction); diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 9a814fb47a..01be0f5ba9 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -613,11 +613,11 @@ namespace MWBase virtual std::string getLocalMapOutputPath() const = 0; ///< Get the local map output path from options or default - virtual void extractWorldMap(const std::string& worldMapOutput) = 0; - ///< Extract world map to the specified directory + virtual void extractWorldMap() = 0; + ///< Extract world map using path from options or default - virtual void extractLocalMaps(const std::string& localMapOutput) = 0; - ///< Extract local maps to the specified directory + virtual void extractLocalMaps() = 0; + ///< Extract local maps using path from options or default virtual bool isMapExtractionActive() const = 0; ///< Check if map extraction is currently in progress diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index dbd842b6f7..978842b4e0 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -247,22 +247,20 @@ namespace MWLua api["vfx"] = initWorldVfxBindings(context); - api["extractWorldMap"] = [context, lua = context.mLua](sol::optional worldMapOutput) { + api["extractWorldMap"] = [context, lua = context.mLua]() { checkGameInitialized(lua); context.mLuaManager->addAction( - [worldMapOutput] { - std::string path = worldMapOutput.value_or(""); - MWBase::Environment::get().getWorld()->extractWorldMap(path); + [] { + MWBase::Environment::get().getWorld()->extractWorldMap(); }, "extractWorldMapAction"); }; - api["extractLocalMaps"] = [context, lua = context.mLua](sol::optional localMapOutput) { + api["extractLocalMaps"] = [context, lua = context.mLua]() { checkGameInitialized(lua); context.mLuaManager->addAction( - [localMapOutput] { - std::string path = localMapOutput.value_or(""); - MWBase::Environment::get().getWorld()->extractLocalMaps(path); + [] { + MWBase::Environment::get().getWorld()->extractLocalMaps(); }, "extractLocalMapsAction"); }; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 16a8067d90..9b92ae4e5b 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -251,7 +251,7 @@ namespace MWWorld } World::World(Resource::ResourceSystem* resourceSystem, int activationDistanceOverride, const std::string& startCell, - const std::filesystem::path& userDataPath) + const std::filesystem::path& userDataPath, const std::string& worldMapOutputPath, const std::string& localMapOutputPath, bool overwriteMaps) : mResourceSystem(resourceSystem) , mLocalScripts(mStore) , mWorldModel(mStore, mReaders) @@ -263,6 +263,9 @@ namespace MWWorld , mUserDataPath(userDataPath) , mActivationDistanceOverride(activationDistanceOverride) , mStartCell(startCell) + , mWorldMapOutputPath(worldMapOutputPath) + , mLocalMapOutputPath(localMapOutputPath) + , mOverwriteMaps(overwriteMaps) , mSwimHeightScale(0.f) , mDistanceToFocusObject(-1.f) , mTeleportEnabled(true) @@ -3913,83 +3916,35 @@ namespace MWWorld actor->setActive(value); } - std::string World::getWorldMapOutputPath() const + void World::extractWorldMap() { - // Try to get from Engine via environment, fallback to default - // Since we can't directly access Engine from World, we'll use a default path - // The actual path from options will be passed through the Lua API - return "./textures/advanced_world_map/custom"; - } - - std::string World::getLocalMapOutputPath() const - { - // Try to get from Engine via environment, fallback to default - // Since we can't directly access Engine from World, we'll use a default path - // The actual path from options will be passed through the Lua API - return "./textures/advanced_world_map/local"; - } - - void World::extractWorldMap(const std::string& worldMapOutput) - { - if (!mRendering) + if (!mMapExtractor) { - throw std::runtime_error("Rendering manager is not initialized"); + mMapExtractor = std::make_unique( + mWorldMapOutputPath, mLocalMapOutputPath, mOverwriteMaps, mRendering.get(), &mStore); } - - osgViewer::Viewer* viewer = mRendering->getViewer(); - if (!viewer) - { - throw std::runtime_error("Viewer is not initialized"); - } - - // If extraction is already in progress, ignore the request - if (mMapExtractor) - { - Log(Debug::Warning) << "Map extraction is already in progress"; - return; - } - - std::string outputPath = worldMapOutput.empty() ? getWorldMapOutputPath() : worldMapOutput; - - MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - mMapExtractor = std::make_unique(*this, viewer, windowManager, outputPath, ""); - - Log(Debug::Info) << "Starting world map extraction to: " << outputPath; mMapExtractor->extractWorldMap(); } - void World::extractLocalMaps(const std::string& localMapOutput) + void World::extractLocalMaps() { - if (!mRendering) + if (!mMapExtractor) { - throw std::runtime_error("Rendering manager is not initialized"); + mMapExtractor = std::make_unique( + mWorldMapOutputPath, mLocalMapOutputPath, mOverwriteMaps, mRendering.get(), &mStore); } - - osgViewer::Viewer* viewer = mRendering->getViewer(); - if (!viewer) + // Set LocalMap from WindowManager + if (auto* localMap = MWBase::Environment::get().getWindowManager()->getLocalMapRender()) { - throw std::runtime_error("Viewer is not initialized"); + mMapExtractor->setLocalMap(localMap); } - - // If extraction is already in progress, ignore the request - if (mMapExtractor) - { - Log(Debug::Warning) << "Map extraction is already in progress"; - return; - } - - std::string outputPath = localMapOutput.empty() ? getLocalMapOutputPath() : localMapOutput; - - MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - mMapExtractor = std::make_unique(*this, viewer, windowManager, "", outputPath); - - Log(Debug::Info) << "Starting local maps extraction to: " << outputPath; - mMapExtractor->extractLocalMaps(false); - Log(Debug::Info) << "Local maps extraction started, will complete during gameplay..."; + const auto& activeCells = mWorldScene->getActiveCells(); + std::vector cells(activeCells.begin(), activeCells.end()); + mMapExtractor->extractLocalMaps(cells); } bool World::isMapExtractionActive() const { - return mMapExtractor != nullptr; + return mMapExtractor && !mMapExtractor->isExtractionComplete(); } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 44989f08bc..cf75443d9e 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -121,6 +121,9 @@ namespace MWWorld int mActivationDistanceOverride; std::string mStartCell; + std::string mWorldMapOutputPath; + std::string mLocalMapOutputPath; + bool mOverwriteMaps; float mSwimHeightScale; @@ -198,7 +201,7 @@ namespace MWWorld void removeContainerScripts(const Ptr& reference) override; World(Resource::ResourceSystem* resourceSystem, int activationDistanceOverride, const std::string& startCell, - const std::filesystem::path& userDataPath); + const std::filesystem::path& userDataPath, const std::string& worldMapOutputPath, const std::string& localMapOutputPath, bool overwriteMaps); void loadData(const Files::Collections& fileCollections, const std::vector& contentFiles, const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, @@ -684,11 +687,12 @@ namespace MWWorld void setActorActive(const MWWorld::Ptr& ptr, bool value) override; - std::string getWorldMapOutputPath() const override; - std::string getLocalMapOutputPath() const override; + std::string getWorldMapOutputPath() const override { return mWorldMapOutputPath; } + std::string getLocalMapOutputPath() const override { return mLocalMapOutputPath; } + bool getOverwriteMaps() const { return mOverwriteMaps; } - void extractWorldMap(const std::string& worldMapOutput) override; - void extractLocalMaps(const std::string& localMapOutput) override; + void extractWorldMap() override; + void extractLocalMaps() override; bool isMapExtractionActive() const override; }; } diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index d1f6f9357d..26720bc448 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -101,8 +101,8 @@ namespace OpenMW addOption("local-map-output", bpo::value()->default_value(""), "directory to save local map textures (default: textures/advanced_world_map/local)"); - addOption("extract-maps", bpo::value()->implicit_value(true)->default_value(true), - "extract world and local map textures and exit"); + addOption("overwrite-maps", bpo::value()->implicit_value(true)->default_value(false), + "overwrite existing map files during extraction"); return desc; } diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index bbf27a7ce4..84edc95874 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -222,22 +222,21 @@ -- @param #number hours Number of hours to advance time --- --- Extract world map to the specified directory. +-- Extract world map using path from --world-map-output option or default path. -- This function generates a world map image and saves it as a PNG file. --- If no path is provided (nil), uses the path from --world-map-output option or default "./textures/advanced_world_map/custom". +-- The output directory is determined by --world-map-output command line option, +-- or defaults to "./textures/advanced_world_map/custom" if not specified. -- @function [parent=#world] extractWorldMap --- @param #string worldMapOutput (optional) Directory path where world map will be saved --- @usage world.extractWorldMap("./maps/world") -- Custom path --- @usage world.extractWorldMap() -- Use default or option path +-- @usage world.extractWorldMap() -- Use path from option or default --- --- Extract local maps to the specified directory. +-- Extract local maps using path from --local-map-output option or default path. -- This function generates map images for all active cells and saves them as PNG files. --- If no path is provided (nil), uses the path from --local-map-output option or default "./textures/advanced_world_map/local". +-- The output directory is determined by --local-map-output command line option, +-- or defaults to "./textures/advanced_world_map/local" if not specified. +-- By default, existing maps are not overwritten. Use --overwrite-maps option to force overwriting. -- @function [parent=#world] extractLocalMaps --- @param #string localMapOutput (optional) Directory path where local maps will be saved --- @usage world.extractLocalMaps("./maps/local") -- Custom path --- @usage world.extractLocalMaps() -- Use default or option path +-- @usage world.extractLocalMaps() -- Use path from option or default --- -- Enable extraction mode for map generation. From a89bd0e40e525d14efa574f1ad361c09e5d87b63 Mon Sep 17 00:00:00 2001 From: Diject Date: Tue, 30 Dec 2025 21:21:27 +0300 Subject: [PATCH 18/23] Add Lua API for map overwrite flag and existing map IDs --- apps/openmw/mwbase/world.hpp | 3 ++ apps/openmw/mwlua/worldbindings.cpp | 45 +++++++++++++++++++++++++++++ apps/openmw/mwworld/worldimp.hpp | 2 +- files/lua_api/openmw/world.lua | 42 +++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 01be0f5ba9..531921f1c8 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -613,6 +613,9 @@ namespace MWBase virtual std::string getLocalMapOutputPath() const = 0; ///< Get the local map output path from options or default + virtual bool getOverwriteMaps() const = 0; + ///< Get the overwrite maps flag + virtual void extractWorldMap() = 0; ///< Extract world map using path from options or default diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index 978842b4e0..4c577b1a69 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -1,5 +1,7 @@ #include "worldbindings.hpp" +#include + #include #include #include @@ -301,6 +303,49 @@ namespace MWLua return MWBase::Environment::get().getWorld()->isMapExtractionActive(); }; + api["getOverwriteFlag"] = [lua = context.mLua]() -> bool { + checkGameInitialized(lua); + return MWBase::Environment::get().getWorld()->getOverwriteMaps(); + }; + + api["getExistingLocalMapIds"] = [lua = context.mLua](sol::this_state luaState) -> sol::table { + checkGameInitialized(lua); + sol::state_view state(luaState); + sol::table result = state.create_table(); + + std::string localMapPath = MWBase::Environment::get().getWorld()->getLocalMapOutputPath(); + std::filesystem::path dir(localMapPath); + + if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) + return result; + + // Use set to store unique filenames (without extension) + std::set uniqueNames; + + for (const auto& entry : std::filesystem::directory_iterator(dir)) + { + if (entry.is_regular_file()) + { + std::string ext = entry.path().extension().string(); + // Check for .yaml, .png, or .tga extensions + if (ext == ".yaml" || ext == ".png" || ext == ".tga") + { + std::string filename = entry.path().stem().string(); + uniqueNames.insert(filename); + } + } + } + + // Convert set to lua table + int index = 1; + for (const auto& name : uniqueNames) + { + result[index++] = name; + } + + return result; + }; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index cf75443d9e..aefd0532ab 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -689,7 +689,7 @@ namespace MWWorld std::string getWorldMapOutputPath() const override { return mWorldMapOutputPath; } std::string getLocalMapOutputPath() const override { return mLocalMapOutputPath; } - bool getOverwriteMaps() const { return mOverwriteMaps; } + bool getOverwriteMaps() const override { return mOverwriteMaps; } void extractWorldMap() override; void extractLocalMaps() override; diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 84edc95874..22a2a5f676 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -265,5 +265,47 @@ -- print("Map extraction already in progress") -- end +--- +-- Get the overwrite maps flag from command line options. +-- Returns true if the --overwrite-maps option was specified, false otherwise. +-- This flag determines whether existing map files should be overwritten during extraction. +-- @function [parent=#world] getOverwriteFlag +-- @return #boolean true if overwrite is enabled, false otherwise +-- @usage +-- if world.getOverwriteFlag() then +-- print("Will overwrite existing maps") +-- else +-- print("Will skip existing maps") +-- end + +--- +-- Get list of existing local map IDs (filenames without extension). +-- Returns a table containing unique names of all files in the local map output directory +-- with .yaml, .png, or .tga extensions, without the extension itself. +-- Each filename corresponds to a cell ID that has been extracted. +-- This can be used to check which maps have already been generated. +-- The list contains unique names - if a cell has multiple file types (e.g., both .yaml and .png), +-- the name will appear only once in the list. +-- @function [parent=#world] getExistingLocalMapIds +-- @return #table Array of strings containing unique local map IDs (cell names without extension) +-- @usage +-- local existingMaps = world.getExistingLocalMapIds() +-- for _, mapId in ipairs(existingMaps) do +-- print("Found existing map: " .. mapId) +-- end +-- +-- -- Check if a specific map exists +-- local targetCell = "Balmora" +-- local exists = false +-- for _, mapId in ipairs(existingMaps) do +-- if mapId == targetCell then +-- exists = true +-- break +-- end +-- end +-- if not exists then +-- print("Map for " .. targetCell .. " not found, need to extract") +-- end + return nil From 63adf0b99187513cb4aca771dae55971ba60d548 Mon Sep 17 00:00:00 2001 From: Diject Date: Thu, 1 Jan 2026 13:43:06 +0300 Subject: [PATCH 19/23] Add map extractor scripts and update script lists --- files/data-mw/CMakeLists.txt | 13 --- files/data-mw/builtin.omwscripts.in | 7 -- files/data/CMakeLists.txt | 40 +------ files/data/builtin.omwscripts | 38 +------ files/data/scripts/map_extractor/global.lua | 115 ++++++++++++++++++++ files/data/scripts/map_extractor/menu.lua | 77 +++++++++++++ files/data/scripts/map_extractor/player.lua | 18 +++ 7 files changed, 218 insertions(+), 90 deletions(-) create mode 100644 files/data/scripts/map_extractor/global.lua create mode 100644 files/data/scripts/map_extractor/menu.lua create mode 100644 files/data/scripts/map_extractor/player.lua diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index 41924315c5..5163b33f4a 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -20,19 +20,6 @@ set(BUILTIN_DATA_MW_FILES # L10n for OpenMW menus and non-game-specific messages l10n/OMWEngine/gmst.yaml - - # Game-specific settings for calendar.lua - openmw_aux/calendarconfig.lua - - scripts/omw/cellhandlers.lua - scripts/omw/combat/common.lua - scripts/omw/combat/global.lua - scripts/omw/combat/local.lua - scripts/omw/combat/menu.lua - scripts/omw/music/helpers.lua - scripts/omw/music/music.lua - scripts/omw/music/settings.lua - scripts/omw/playerskillhandlers.lua ) foreach (f ${BUILTIN_DATA_MW_FILES}) diff --git a/files/data-mw/builtin.omwscripts.in b/files/data-mw/builtin.omwscripts.in index ffcadbf69f..3f7cd71ba6 100644 --- a/files/data-mw/builtin.omwscripts.in +++ b/files/data-mw/builtin.omwscripts.in @@ -1,10 +1,3 @@ @BUILTIN_SCRIPTS@ # Game specific scripts to append to builtin.omwscripts -GLOBAL: scripts/omw/cellhandlers.lua -GLOBAL: scripts/omw/combat/global.lua -MENU: scripts/omw/combat/menu.lua -NPC,CREATURE,PLAYER: scripts/omw/combat/local.lua -PLAYER: scripts/omw/music/music.lua -MENU: scripts/omw/music/settings.lua -PLAYER: scripts/omw/playerskillhandlers.lua diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 4b1b9e01ca..cfec946e27 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -122,47 +122,15 @@ set(BUILTIN_DATA_FILES openmw_aux/ui.lua builtin.omwscripts + + scripts/map_extractor/global.lua + scripts/map_extractor/menu.lua + scripts/map_extractor/player.lua - scripts/omw/activationhandlers.lua - scripts/omw/ai.lua - scripts/omw/camera/camera.lua - scripts/omw/camera/head_bobbing.lua - scripts/omw/camera/third_person.lua - scripts/omw/camera/settings.lua - scripts/omw/camera/move360.lua - scripts/omw/camera/first_person_auto_switch.lua scripts/omw/console/global.lua scripts/omw/console/local.lua scripts/omw/console/player.lua scripts/omw/console/menu.lua - scripts/omw/combat/interface.lua - scripts/omw/mechanics/actorcontroller.lua - scripts/omw/mechanics/animationcontroller.lua - scripts/omw/mechanics/globalcontroller.lua - scripts/omw/mechanics/playercontroller.lua - scripts/omw/settings/menu.lua - scripts/omw/music/actor.lua - scripts/omw/settings/player.lua - scripts/omw/settings/global.lua - scripts/omw/settings/common.lua - scripts/omw/settings/renderers.lua - scripts/omw/mwui/constants.lua - scripts/omw/mwui/borders.lua - scripts/omw/mwui/filters.lua - scripts/omw/mwui/text.lua - scripts/omw/mwui/textEdit.lua - scripts/omw/mwui/space.lua - scripts/omw/mwui/init.lua - scripts/omw/skillhandlers.lua - scripts/omw/crimes.lua - scripts/omw/ui.lua - scripts/omw/usehandlers.lua - scripts/omw/worldeventhandlers.lua - scripts/omw/input/settings.lua - scripts/omw/input/playercontrols.lua - scripts/omw/input/actionbindings.lua - scripts/omw/input/smoothmovement.lua - scripts/omw/input/gamepadcontrols.lua shaders/adjustments.omwfx shaders/bloomlinear.omwfx diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 6a3899accd..da6a59fce4 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -1,39 +1,9 @@ -# UI framework -MENU,PLAYER: scripts/omw/mwui/init.lua - -# Settings framework -MENU: scripts/omw/settings/menu.lua -PLAYER: scripts/omw/settings/player.lua -GLOBAL: scripts/omw/settings/global.lua - -# Mechanics -GLOBAL: scripts/omw/activationhandlers.lua -GLOBAL: scripts/omw/usehandlers.lua -GLOBAL: scripts/omw/worldeventhandlers.lua -GLOBAL: scripts/omw/crimes.lua -CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua -PLAYER: scripts/omw/skillhandlers.lua -PLAYER: scripts/omw/mechanics/playercontroller.lua -MENU: scripts/omw/camera/settings.lua -MENU: scripts/omw/input/settings.lua -PLAYER: scripts/omw/input/playercontrols.lua -PLAYER: scripts/omw/camera/camera.lua -PLAYER: scripts/omw/input/actionbindings.lua -PLAYER: scripts/omw/input/smoothmovement.lua -PLAYER: scripts/omw/input/gamepadcontrols.lua -NPC,CREATURE: scripts/omw/ai.lua -GLOBAL: scripts/omw/mechanics/globalcontroller.lua -CREATURE, NPC, PLAYER: scripts/omw/mechanics/actorcontroller.lua -NPC,CREATURE,PLAYER: scripts/omw/combat/interface.lua - -# User interface -PLAYER: scripts/omw/ui.lua +PLAYER: scripts/map_extractor/player.lua +GLOBAL: scripts/map_extractor/global.lua +MENU: scripts/map_extractor/menu.lua # Lua console MENU: scripts/omw/console/menu.lua PLAYER: scripts/omw/console/player.lua GLOBAL: scripts/omw/console/global.lua -CUSTOM: scripts/omw/console/local.lua - -# Music system -NPC,CREATURE: scripts/omw/music/actor.lua +CUSTOM: scripts/omw/console/local.lua \ No newline at end of file diff --git a/files/data/scripts/map_extractor/global.lua b/files/data/scripts/map_extractor/global.lua new file mode 100644 index 0000000000..e900b47f9d --- /dev/null +++ b/files/data/scripts/map_extractor/global.lua @@ -0,0 +1,115 @@ +local util = require("openmw.util") +local world = require("openmw.world") +local async = require("openmw.async") + +local visitedCells = {} + +local cellCount = #world.cells +local i = cellCount + + +local function getExCellId(gridX, gridY) + return string.format("(%d,%d)", gridX, gridY) +end + + +local function getCellId(cell) + if not cell then return end + if cell.isExterior then + return getExCellId(cell.gridX, cell.gridY) + else + return (cell.id or ""):gsub(":", "") + end +end + + +local function processAndTeleport(skipExtraction) + local pl = world.players[1] + + if not skipExtraction then + world.extractLocalMaps() + end + + local function func() + if world.isMapExtractionActive() then + async:newUnsavableSimulationTimer(0.05, func) + return + elseif skipExtraction then + pl:sendEvent("builtin:map_extractor:updateMenu", { + line1 = "Generating local maps...", + }) + end + + repeat + local res, cell = pcall(function () + return world.cells[i] + end) + if not res then cell = nil end + i = i - 1 + + local pos + + local customCellId = getCellId(cell) + if not cell or not customCellId or visitedCells[customCellId] then goto continue end + + visitedCells[customCellId] = true + if cell.isExterior then + for j = cell.gridX - 1, cell.gridX + 1 do + for k = cell.gridY - 1, cell.gridY + 1 do + visitedCells[getExCellId(j, k)] = true + end + end + end + + if cell.isExterior then + pos = util.vector3(cell.gridX * 8192 + 4096, cell.gridY * 8192 + 4096, 0) + else + pos = util.vector3(0, 0, 0) + end + + do + pl:teleport(cell, pos) + pl:sendEvent("builtin:map_extractor:updateMenu", { + line2 = string.format("Processed %d / %d cells", cellCount - i, cellCount), + }) + break + end + + ::continue:: + until i <= 0 + + if i <= 0 then + pl:sendEvent("builtin:map_extractor:updateMenu", { + line1 = "Map extraction complete.", + line2 = "", + }) + end + end + + async:newUnsavableSimulationTimer(0.05, func) +end + + +async:newUnsavableSimulationTimer(0.1, function () + world.players[1]:sendEvent("builtin:map_extractor:updateMenu", {line1 = "Generating world map..."}) + world.enableExtractionMode() + world.extractWorldMap() + + if not world.getOverwriteFlag() then + for _, cellId in pairs(world.getExistingLocalMapIds() or {}) do + visitedCells[cellId] = true + end + end + + processAndTeleport(true) +end) + + + +return { + eventHandlers = { + ["builtin:map_extractor:teleport"] = function (pl) + processAndTeleport() + end, + } +} \ No newline at end of file diff --git a/files/data/scripts/map_extractor/menu.lua b/files/data/scripts/map_extractor/menu.lua new file mode 100644 index 0000000000..be96329d70 --- /dev/null +++ b/files/data/scripts/map_extractor/menu.lua @@ -0,0 +1,77 @@ +local ui = require("openmw.ui") +local util = require("openmw.util") + + +ui.layers.insertAfter("MainMenuBackground", "builtin:map_extractor", {interactive = false}) + +local content = ui.content{ + { + type = ui.TYPE.Text, + props = { + text = "", + textSize = 20, + autoSize = true, + textColor = util.color.rgb(1, 1, 1), + textAlignH = ui.ALIGNMENT.Center, + textAlignV = ui.ALIGNMENT.Center, + textShadow = true, + textShadowColor = util.color.rgb(0, 0, 0), + anchor = util.vector2(0.5, 0.5), + }, + }, + { + type = ui.TYPE.Text, + props = { + text = "", + textSize = 20, + autoSize = true, + textColor = util.color.rgb(1, 1, 1), + textAlignH = ui.ALIGNMENT.Center, + textAlignV = ui.ALIGNMENT.Center, + textShadow = true, + textShadowColor = util.color.rgb(0, 0, 0), + anchor = util.vector2(0.5, 0.5), + }, + } +} + + +local layout = { + type = ui.TYPE.Container, + layer = "builtin:map_extractor", + props = { + anchor = util.vector2(0.5, 0.5), + relativePosition = util.vector2(0.5, 0.5), + }, + content = ui.content{ + { + type = ui.TYPE.Flex, + props = { + align = ui.ALIGNMENT.Center, + arrange = ui.ALIGNMENT.Center, + }, + content = content, + } + }, +} + + +local menu = ui.create(layout) + + +return { + eventHandlers = { + ["builtin:map_extractor:updateMenu"] = function (data) + if not data then data = {} end + + if data.line1 then + content[1].props.text = data.line1 + end + if data.line2 then + content[2].props.text = data.line2 + end + + menu:update() + end, + } +} \ No newline at end of file diff --git a/files/data/scripts/map_extractor/player.lua b/files/data/scripts/map_extractor/player.lua new file mode 100644 index 0000000000..74db60e8b5 --- /dev/null +++ b/files/data/scripts/map_extractor/player.lua @@ -0,0 +1,18 @@ +local self = require("openmw.self") +local core = require("openmw.core") +local types = require("openmw.types") + + + +return { + engineHandlers = { + onTeleported = function () + core.sendGlobalEvent("builtin:map_extractor:teleport", self) + end + }, + eventHandlers = { + ["builtin:map_extractor:updateMenu"] = function (data) + types.Player.sendMenuEvent(self, "builtin:map_extractor:updateMenu", data) + end, + } +} \ No newline at end of file From ff10bf57781cd4035894f4bf081d195a0d04d81f Mon Sep 17 00:00:00 2001 From: Diject Date: Thu, 1 Jan 2026 17:00:00 +0300 Subject: [PATCH 20/23] Update mapextractor output format and add bounds --- apps/openmw/mapextractor.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index 86578ee1eb..ef44a15d45 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -655,8 +655,6 @@ namespace OMW float oY = (rotatedY - min.y()) / mapWorldSize * 256.0f; float totalHeight = segmentsY * 256.0f; - float mX = oX * 2.0f; - float mY = (oY - totalHeight) * 2.0f; std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); std::ofstream file(yamlPath); @@ -667,13 +665,20 @@ namespace OMW return; } + // TODO: Replace hardcoded padding with a constant from LocalMap + // Padding value taken from the implementation in localmap.cpp. + // Used to shrink the bounds to match the one used during cell rendering. + const float padding = 500.0f; + + file << "v: 2\n"; file << "nA: " << nA << "\n"; - file << "mX: " << mX << "\n"; - file << "mY: " << mY << "\n"; file << "oX: " << oX << "\n"; file << "oY: " << oY << "\n"; - file << "width: " << segmentsX << "\n"; - file << "height: " << segmentsY << "\n"; + file << "wT: " << segmentsX << "\n"; + file << "hT: " << segmentsY << "\n"; + file << "mBnds:\n"; + file << " min: [" << bounds.xMin() + padding << ", " << bounds.yMin() + padding << ", " << bounds.zMin() << "]\n"; + file << " max: [" << bounds.xMax() - padding << ", " << bounds.yMax() - padding << ", " << bounds.zMax() << "]\n"; file.close(); } From f4bb0a69cacfe07467ad928c288ea1187a2454f2 Mon Sep 17 00:00:00 2001 From: Diject Date: Thu, 1 Jan 2026 17:06:03 +0300 Subject: [PATCH 21/23] Remove unnecessary debug log message --- apps/openmw/mwworld/worldimp.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 9b92ae4e5b..89932b5d72 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1669,7 +1669,6 @@ namespace MWWorld mMapExtractor->update(); if (mMapExtractor->isExtractionComplete()) { - Log(Debug::Info) << "Map extraction complete."; mMapExtractor.reset(); } } From 80d404f4d8334b3f5595c3f583e78d3141986fca Mon Sep 17 00:00:00 2001 From: Diject Date: Thu, 1 Jan 2026 17:47:18 +0300 Subject: [PATCH 22/23] Update default options --- apps/openmw/engine.cpp | 4 ++-- apps/openmw/main.cpp | 8 +++----- apps/openmw/options.cpp | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 2dec1d478e..162c2cdfb3 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -483,8 +483,8 @@ void OMW::Engine::addGroundcoverFile(const std::string& file) void OMW::Engine::setSkipMenu(bool skipMenu, bool newGame) { - mSkipMenu = true; - mNewGame = false; + mSkipMenu = skipMenu; + mNewGame = newGame; } void OMW::Engine::createWindow() diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 1f0d7c2e56..f50da3312c 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -73,7 +73,7 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati MWGui::DebugWindow::startLogRecording(); - engine.setGrabMouse(!variables["no-grab"].as()); + engine.setGrabMouse(false); // Font encoding settings std::string encoding(variables["encoding"].as()); @@ -146,9 +146,7 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati // startup-settings engine.setCell(variables["start"].as()); - engine.setSkipMenu(variables["skip-menu"].as(), variables["new-game"].as()); - if (!variables["skip-menu"].as() && variables["new-game"].as()) - Log(Debug::Warning) << "Warning: new-game used without skip-menu -> ignoring it"; + engine.setSkipMenu(true, false); // scripts engine.setCompileAll(variables["script-all"].as()); @@ -160,7 +158,7 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati // other settings Fallback::Map::init(variables["fallback"].as().mMap); - engine.setSoundUsage(!variables["no-sound"].as()); + engine.setSoundUsage(false); engine.setActivationDistanceOverride(variables["activate-dist"].as()); engine.enableFontExport(variables["export-fonts"].as()); engine.setRandomSeed(variables["random-seed"].as()); diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index 26720bc448..356a98bb79 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -44,7 +44,7 @@ namespace OpenMW bpo::value()->default_value(StringsVector(), "")->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon"); - addOption("no-sound", bpo::value()->implicit_value(true)->default_value(true), "disable all sounds"); + addOption("no-sound", bpo::value()->implicit_value(true)->default_value(false), "disable all sounds"); addOption("script-all", bpo::value()->implicit_value(true)->default_value(false), "compile all scripts (excluding dialogue scripts) at startup"); @@ -68,7 +68,7 @@ namespace OpenMW "load a save game file on game startup (specify an absolute filename or a filename relative to the current " "working directory)"); - addOption("skip-menu", bpo::value()->implicit_value(true)->default_value(true), + addOption("skip-menu", bpo::value()->implicit_value(true)->default_value(false), "skip main menu on game startup"); addOption("new-game", bpo::value()->implicit_value(true)->default_value(false), @@ -85,7 +85,7 @@ namespace OpenMW bpo::value()->default_value(Fallback::FallbackMap(), "")->multitoken()->composing(), "fallback values"); - addOption("no-grab", bpo::value()->implicit_value(true)->default_value(true), "Don't grab mouse cursor"); + addOption("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor"); addOption("export-fonts", bpo::value()->implicit_value(true)->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory"); From 51e6fdbcb442936a9d630f4ea3c8b8c78d73b950 Mon Sep 17 00:00:00 2001 From: Diject Date: Thu, 1 Jan 2026 17:49:32 +0300 Subject: [PATCH 23/23] Add estimated time and improve map extraction UI --- files/data/scripts/map_extractor/global.lua | 46 +++++++++++++++++---- files/data/scripts/map_extractor/menu.lua | 38 +++++++++-------- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/files/data/scripts/map_extractor/global.lua b/files/data/scripts/map_extractor/global.lua index e900b47f9d..ce503b5997 100644 --- a/files/data/scripts/map_extractor/global.lua +++ b/files/data/scripts/map_extractor/global.lua @@ -1,11 +1,15 @@ local util = require("openmw.util") local world = require("openmw.world") local async = require("openmw.async") +local core = require("openmw.core") +local types = require("openmw.types") local visitedCells = {} local cellCount = #world.cells local i = cellCount +local lastTimestamp = core.getRealTime() - 100 +local timeFromLast = 100 local function getExCellId(gridX, gridY) @@ -41,6 +45,12 @@ local function processAndTeleport(skipExtraction) end repeat + if i % 50 == 0 then + local currentTime = core.getRealTime() + timeFromLast = (timeFromLast + currentTime - lastTimestamp) / 2 + lastTimestamp = currentTime + end + local res, cell = pcall(function () return world.cells[i] end) @@ -68,10 +78,18 @@ local function processAndTeleport(skipExtraction) end do - pl:teleport(cell, pos) + local estimatedTimeLeft = math.max(timeFromLast, 1) / 50 * i + local hours = math.floor(estimatedTimeLeft / 3600) + estimatedTimeLeft = estimatedTimeLeft % 3600 + local minutes = math.floor(estimatedTimeLeft / 60) + local seconds = estimatedTimeLeft % 60 pl:sendEvent("builtin:map_extractor:updateMenu", { line2 = string.format("Processed %d / %d cells", cellCount - i, cellCount), + line3 = string.format("Estimated time left: %d:%02d:%02.0f", hours, minutes, seconds), }) + + pl:teleport(cell, pos) + break end @@ -82,6 +100,7 @@ local function processAndTeleport(skipExtraction) pl:sendEvent("builtin:map_extractor:updateMenu", { line1 = "Map extraction complete.", line2 = "", + line3 = "", }) end end @@ -91,17 +110,28 @@ end async:newUnsavableSimulationTimer(0.1, function () - world.players[1]:sendEvent("builtin:map_extractor:updateMenu", {line1 = "Generating world map..."}) + local pl = world.players[1] + pl:sendEvent("builtin:map_extractor:updateMenu", {line1 = "Generating world map..."}) world.enableExtractionMode() - world.extractWorldMap() + types.Player.setControlSwitch(pl, types.Player.CONTROL_SWITCH.Controls, false) + types.Player.setControlSwitch(pl, types.Player.CONTROL_SWITCH.Fighting, false) + types.Player.setControlSwitch(pl, types.Player.CONTROL_SWITCH.Jumping, false) + types.Player.setControlSwitch(pl, types.Player.CONTROL_SWITCH.Looking, false) + types.Player.setControlSwitch(pl, types.Player.CONTROL_SWITCH.Magic, false) + types.Player.setControlSwitch(pl, types.Player.CONTROL_SWITCH.VanityMode, false) + types.Player.setControlSwitch(pl, types.Player.CONTROL_SWITCH.ViewMode, false) - if not world.getOverwriteFlag() then - for _, cellId in pairs(world.getExistingLocalMapIds() or {}) do - visitedCells[cellId] = true + async:newUnsavableSimulationTimer(0.1, function () + world.extractWorldMap() + + if not world.getOverwriteFlag() then + for _, cellId in pairs(world.getExistingLocalMapIds() or {}) do + visitedCells[cellId] = true + end end - end - processAndTeleport(true) + processAndTeleport(true) + end) end) diff --git a/files/data/scripts/map_extractor/menu.lua b/files/data/scripts/map_extractor/menu.lua index be96329d70..7d8e21ee7a 100644 --- a/files/data/scripts/map_extractor/menu.lua +++ b/files/data/scripts/map_extractor/menu.lua @@ -2,25 +2,15 @@ local ui = require("openmw.ui") local util = require("openmw.util") +local screenSize = ui.layers[ui.layers.indexOf("HUD")].size + + ui.layers.insertAfter("MainMenuBackground", "builtin:map_extractor", {interactive = false}) -local content = ui.content{ - { - type = ui.TYPE.Text, - props = { - text = "", - textSize = 20, - autoSize = true, - textColor = util.color.rgb(1, 1, 1), - textAlignH = ui.ALIGNMENT.Center, - textAlignV = ui.ALIGNMENT.Center, - textShadow = true, - textShadowColor = util.color.rgb(0, 0, 0), - anchor = util.vector2(0.5, 0.5), - }, - }, - { - type = ui.TYPE.Text, + +local function textLine() + return { + type = ui.TYPE.TextEdit, props = { text = "", textSize = 20, @@ -31,8 +21,19 @@ local content = ui.content{ textShadow = true, textShadowColor = util.color.rgb(0, 0, 0), anchor = util.vector2(0.5, 0.5), + size = util.vector2(screenSize.x, 0), + multiline = true, + wordWrap = true, + readOnly = true, }, } +end + + +local content = ui.content{ + textLine(), + textLine(), + textLine(), } @@ -70,6 +71,9 @@ return { if data.line2 then content[2].props.text = data.line2 end + if data.line3 then + content[3].props.text = data.line3 + end menu:update() end,