1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2026-01-24 15:00:55 +00:00

Merge pull request #2 from Diject/dev

Update
This commit is contained in:
Diject 2026-01-01 17:55:03 +03:00 committed by GitHub
commit a0eea54c21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1620 additions and 136 deletions

15
CMakeSettings.json Normal file
View file

@ -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": ""
}
]
}

View file

@ -1,6 +1,7 @@
set(OPENMW_SOURCES
engine.cpp
options.cpp
mapextractor.cpp
)
set(OPENMW_RESOURCES

View file

@ -83,6 +83,7 @@
#include "mwstate/statemanagerimp.hpp"
#include "mapextractor.hpp"
#include "profile.hpp"
namespace
@ -365,19 +366,20 @@ 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)
, mCfgMgr(configurationManager)
, mGlMaxTextureImageUnits(0)
, mOverwriteMaps(false)
{
#if SDL_VERSION_ATLEAST(2, 24, 0)
SDL_SetHint(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, "1");
@ -836,7 +838,8 @@ void OMW::Engine::prepareEngine()
// Create the world
mWorld = std::make_unique<MWWorld::World>(
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());
@ -960,11 +963,11 @@ void OMW::Engine::go()
prepareEngine();
#ifdef _WIN32
#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)
@ -994,6 +997,11 @@ void OMW::Engine::go()
if (stats.is_open())
Resource::collectStatistics(*mViewer);
// Map extractor
std::unique_ptr<MapExtractor> mapExtractor;
// Start the game
if (!mSaveGameFile.empty())
{
@ -1043,6 +1051,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) << "Extraction process completed.";
mapExtractor.reset();
}
}
timeManager.updateIsPaused();
if (!timeManager.isPaused())
{
@ -1130,3 +1150,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::setOverwriteMaps(bool overwrite)
{
mOverwriteMaps = overwrite;
}

View file

@ -183,6 +183,10 @@ namespace OMW
Translation::Storage mTranslationDataStorage;
bool mNewGame;
std::string mWorldMapOutput;
std::string mLocalMapOutput;
bool mOverwriteMaps;
Files::ConfigurationManager& mCfgMgr;
int mGlMaxTextureImageUnits;
@ -264,6 +268,14 @@ namespace OMW
void setRandomSeed(unsigned int seed);
void setWorldMapOutput(const std::string& path);
void setLocalMapOutput(const std::string& path);
void setOverwriteMaps(bool overwrite);
void setExtractMaps(bool extract);
void setRecastMaxLogLevel(Debug::Level value) { mMaxRecastLogLevel = value; }
};
}

View file

@ -2,6 +2,7 @@
#include <components/fallback/fallback.hpp>
#include <components/fallback/validate.hpp>
#include <components/files/configurationmanager.hpp>
#include <components/files/conversion.hpp>
#include <components/misc/osgpluginchecker.hpp>
#include <components/misc/rng.hpp>
#include <components/platform/platform.hpp>
@ -72,7 +73,7 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati
MWGui::DebugWindow::startLogRecording();
engine.setGrabMouse(!variables["no-grab"].as<bool>());
engine.setGrabMouse(false);
// Font encoding settings
std::string encoding(variables["encoding"].as<std::string>());
@ -122,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<StringsVector>();
for (auto& file : groundcover)
{
engine.addGroundcoverFile(file);
}
Log(Debug::Warning) << "Skipping groundcover files.";
//StringsVector groundcover = variables["groundcover"].as<StringsVector>();
//for (auto& file : groundcover)
//{
// engine.addGroundcoverFile(file);
//}
if (variables.count("lua-scripts"))
{
@ -139,9 +146,7 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati
// startup-settings
engine.setCell(variables["start"].as<std::string>());
engine.setSkipMenu(variables["skip-menu"].as<bool>(), variables["new-game"].as<bool>());
if (!variables["skip-menu"].as<bool>() && variables["new-game"].as<bool>())
Log(Debug::Warning) << "Warning: new-game used without skip-menu -> ignoring it";
engine.setSkipMenu(true, false);
// scripts
engine.setCompileAll(variables["script-all"].as<bool>());
@ -153,11 +158,34 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati
// other settings
Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().mMap);
engine.setSoundUsage(!variables["no-sound"].as<bool>());
engine.setSoundUsage(false);
engine.setActivationDistanceOverride(variables["activate-dist"].as<int>());
engine.enableFontExport(variables["export-fonts"].as<bool>());
engine.setRandomSeed(variables["random-seed"].as<unsigned int>());
std::string worldMapOutput = variables["world-map-output"].as<std::string>();
std::string localMapOutput = variables["local-map-output"].as<std::string>();
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())
localMapOutput = Files::pathToUnicodeString(std::filesystem::current_path() / "textures" / "advanced_world_map" / "local");
engine.setWorldMapOutput(worldMapOutput);
engine.setLocalMapOutput(localMapOutput);
engine.setOverwriteMaps(variables["overwrite-maps"].as<bool>());
return true;
}
@ -165,13 +193,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)
{

View file

@ -0,0 +1,685 @@
#include "mapextractor.hpp"
#include <algorithm>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <map>
#include <thread>
#include <osg/ComputeBoundsVisitor>
#include <osg/Group>
#include <osg/Image>
#include <osg/Texture2D>
#include <osgDB/WriteFile>
#include <osgViewer/Viewer>
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadcell.hpp>
#include <components/misc/constants.hpp>
#include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/workqueue.hpp>
#include <components/settings/values.hpp>
#include "mwbase/environment.hpp"
#include "mwbase/mechanicsmanager.hpp"
#include "mwbase/windowmanager.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(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())
std::filesystem::create_directories(mWorldMapOutputDir);
if (!mLocalMapOutputDir.empty())
std::filesystem::create_directories(mLocalMapOutputDir);
// Create GlobalMap instance
if (!mRenderingManager)
{
Log(Debug::Error) << "RenderingManager is null in MapExtractor constructor";
throw std::runtime_error("RenderingManager is null");
}
osg::Group* lightRoot = mRenderingManager->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 = mRenderingManager->getWorkQueue();
if (!workQueue)
{
Log(Debug::Error) << "WorkQueue is null in MapExtractor constructor";
throw std::runtime_error("WorkQueue is null");
}
try
{
mGlobalMap = std::make_unique<MWRender::GlobalMap>(root, workQueue);
// LocalMap will be set later via setLocalMap() method
mLocalMap = nullptr;
}
catch (const std::exception& e)
{
Log(Debug::Error) << "Failed to create map objects: " << e.what();
throw;
}
}
MapExtractor::~MapExtractor() = default;
void MapExtractor::extractWorldMap()
{
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";
throw std::runtime_error("Global map not initialized");
}
// 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<osg::Texture2D> 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();
int minX = std::numeric_limits<int>::max();
int maxX = std::numeric_limits<int>::min();
int minY = std::numeric_limits<int>::max();
int maxY = std::numeric_limits<int>::min();
MWWorld::Store<ESM::Cell>::iterator it = mStore->get<ESM::Cell>().extBegin();
for (; it != mStore->get<ESM::Cell>().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(const std::vector<const MWWorld::CellStore*>& activeCells)
{
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);
if (!mLocalMap)
{
Log(Debug::Error) << "Local map not initialized - cannot extract local maps";
return;
}
Log(Debug::Info) << "LocalMap instance is available, starting extraction";
mFramesToWait = 10; // Wait 10 frames before checking (increased from 3)
startExtraction(activeCells);
}
void MapExtractor::startExtraction(const std::vector<const MWWorld::CellStore*>& activeCells)
{
// Enable extraction mode to prevent automatic camera cleanup
if (mLocalMap)
{
mLocalMap->setExtractionMode(true);
}
Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells...";
mPendingExtractions.clear();
for (const MWWorld::CellStore* cellStore : activeCells)
{
if (cellStore->getCell()->isExterior())
{
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 (!mForceOverwrite && 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
{
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 (!mForceOverwrite && 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);
}
}
if (!mPendingExtractions.empty())
{
Log(Debug::Info) << "Queued " << mPendingExtractions.size() << " cells for extraction";
processNextCell();
}
else
{
Log(Debug::Info) << "No cells to extract";
// Clean up any cameras that may have been created
if (mLocalMap)
{
mLocalMap->cleanupCameras();
mLocalMap->setExtractionMode(false);
}
}
}
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<osg::Image> 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<osg::Image> 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());
// 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
{
// 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());
// Clean up cameras even after timeout
if (mLocalMap)
mLocalMap->cleanupCameras();
if (!mPendingExtractions.empty())
{
processNextCell();
}
else
{
// 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<MWWorld::CellStore*>(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)
{
int x = cellStore->getCell()->getGridX();
int y = cellStore->getCell()->getGridY();
std::ostringstream filename;
filename << "(" << x << "," << y << ").png";
std::filesystem::path outputPath = mLocalMapOutputDir / filename.str();
osg::ref_ptr<osg::Image> image = mLocalMap->getMapImage(x, y);
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 << ") - size: "
<< image->s() << "x" << image->t();
return false;
}
osg::ref_ptr<osg::Image> outputImage = new osg::Image(*image, osg::CopyOp::DEEP_COPY_ALL);
if (outputImage->s() != 256 || outputImage->t() != 256)
{
osg::ref_ptr<osg::Image> 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) << "Successfully saved local map for cell (" << x << "," << y << ") to " << outputPath;
return true;
}
else
{
Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ") to " << outputPath;
return false;
}
}
bool MapExtractor::extractInteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite)
{
ESM::RefId cellId = cellStore->getCell()->getId();
std::string cellName(cellStore->getCell()->getNameId());
Log(Debug::Info) << "Saving interior cell: " << cellName;
saveInteriorCellTextures(cellId, cellName);
return true;
}
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);
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;
// Cache images and find bounds
std::map<std::pair<int, int>, osg::ref_ptr<osg::Image>> imageCache;
for (int x = grid.left; x < grid.right; ++x)
{
for (int y = grid.top; y < grid.bottom; ++y)
{
// Get the rendered image directly from camera attachment
osg::ref_ptr<osg::Image> image = mLocalMap->getMapImage(x, y);
if (image && image->s() > 0 && image->t() > 0 && image->data() != nullptr)
{
imageCache[{x, y}] = image;
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)
{
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;
osg::ref_ptr<osg::Image> 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 = minX; x <= maxX; ++x)
{
for (int y = minY; y <= maxY; ++y)
{
auto it = imageCache.find({x, y});
if (it == imageCache.end())
continue;
osg::ref_ptr<osg::Image> segmentImage = it->second;
int segWidth = segmentImage->s();
int segHeight = segmentImage->t();
int destX = (x - minX) * 256;
int destY = (y - minY) * 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");
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);
}
void MapExtractor::saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId,
int segmentsX, int segmentsY)
{
// 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();
osg::Vec2f min(bounds.xMin(), bounds.yMin());
const float mapWorldSize = Constants::CellSizeInUnits;
// 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;
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;
}
// 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 << "oX: " << oX << "\n";
file << "oY: " << oY << "\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();
}
}

View file

@ -0,0 +1,103 @@
#ifndef OPENMW_APPS_OPENMW_MAPEXTRACTOR_HPP
#define OPENMW_APPS_OPENMW_MAPEXTRACTOR_HPP
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include <functional>
#include <components/esm/refid.hpp>
namespace osg
{
class Group;
}
namespace osgViewer
{
class Viewer;
}
namespace SceneUtil
{
class WorkQueue;
}
namespace MWRender
{
class GlobalMap;
class LocalMap;
class RenderingManager;
}
namespace MWWorld
{
class World;
class CellStore;
class ESMStore;
}
namespace MWBase
{
class WindowManager;
}
namespace OMW
{
class MapExtractor
{
public:
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(const std::vector<const MWWorld::CellStore*>& activeCells);
// 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<void()> completionCallback;
};
std::filesystem::path mWorldMapOutputDir;
std::filesystem::path mLocalMapOutputDir;
MWRender::RenderingManager* mRenderingManager;
const MWWorld::ESMStore* mStore;
std::unique_ptr<MWRender::GlobalMap> mGlobalMap;
MWRender::LocalMap* mLocalMap;
std::vector<PendingExtraction> mPendingExtractions;
int mFramesToWait;
bool mForceOverwrite;
void saveWorldMapTexture();
void saveWorldMapInfo();
void startExtraction(const std::vector<const MWWorld::CellStore*>& activeCells);
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);
void saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId,
int segmentsX, int segmentsY);
};
}
#endif

View file

@ -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<std::string_view> getAllWindowIds() const = 0;
virtual std::vector<std::string_view> getAllowedWindowIds(MWGui::GuiMode mode) const = 0;
// For map extraction
virtual MWRender::LocalMap* getLocalMapRender() = 0;
};
}

View file

@ -606,6 +606,24 @@ namespace MWBase
virtual MWWorld::DateTimeManager* getTimeManager() = 0;
virtual void setActorActive(const MWWorld::Ptr& ptr, bool value) = 0;
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 bool getOverwriteMaps() const = 0;
///< Get the overwrite maps flag
virtual void extractWorldMap() = 0;
///< Extract world map using path from options or default
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
};
}

View file

@ -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)

View file

@ -1040,8 +1040,8 @@ namespace MWGui
mToolTips->onFrame(frameDuration);
if (mLocalMapRender)
mLocalMapRender->cleanupCameras();
//if (mLocalMapRender)
// mLocalMapRender->cleanupCameras();
mDebugWindow->onFrame(frameDuration);
@ -2697,4 +2697,9 @@ namespace MWGui
else
mInventoryTabsOverlay->setVisible(false);
}
MWRender::LocalMap* WindowManager::getLocalMapRender()
{
return mLocalMapRender.get();
}
}

View file

@ -409,6 +409,9 @@ namespace MWGui
std::vector<std::string_view> getAllWindowIds() const override;
std::vector<std::string_view> getAllowedWindowIds(GuiMode mode) const override;
// For map extraction
MWRender::LocalMap* getLocalMapRender() override;
private:
unsigned int mOldUpdateMask;
unsigned int mOldCullMask;

View file

@ -1,5 +1,7 @@
#include "worldbindings.hpp"
#include <set>
#include <components/esm3/loadacti.hpp>
#include <components/esm3/loadalch.hpp>
#include <components/esm3/loadarmo.hpp>
@ -247,6 +249,103 @@ namespace MWLua
api["vfx"] = initWorldVfxBindings(context);
api["extractWorldMap"] = [context, lua = context.mLua]() {
checkGameInitialized(lua);
context.mLuaManager->addAction(
[] {
MWBase::Environment::get().getWorld()->extractWorldMap();
},
"extractWorldMapAction");
};
api["extractLocalMaps"] = [context, lua = context.mLua]() {
checkGameInitialized(lua);
context.mLuaManager->addAction(
[] {
MWBase::Environment::get().getWorld()->extractLocalMaps();
},
"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");
};
api["isMapExtractionActive"] = [lua = context.mLua]() -> bool {
checkGameInitialized(lua);
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<std::string> 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);
}
}

View file

@ -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<int>(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<int>(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);
}
}
}
}

View file

@ -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<osg::Texture2D*>(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<osg::Image> 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<osg::Image>();
MapSegment& segment = found->second;
if (!segment.mRTT)
return osg::ref_ptr<osg::Image>();
osg::Camera* camera = segment.mRTT->getCamera(nullptr);
if (!camera)
return osg::ref_ptr<osg::Image>();
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<osg::Image>();
}
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)
{
@ -774,6 +820,21 @@ namespace MWRender
camera->addChild(lightSource);
camera->addChild(mSceneRoot);
// 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<osg::Image> image = new osg::Image;
// 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;
// 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)

View file

@ -62,6 +62,12 @@ namespace MWRender
osg::ref_ptr<osg::Texture2D> getMapTexture(int x, int y);
osg::ref_ptr<osg::Texture2D> 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<osg::Image> 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<osg::Group> mRoot;
@ -132,6 +165,7 @@ namespace MWRender
osg::ref_ptr<osg::Texture2D> mMapTexture;
osg::ref_ptr<osg::Texture2D> mFogOfWarTexture;
osg::ref_ptr<osg::Image> mFogOfWarImage;
osg::ref_ptr<LocalMapRenderToTexture> mRTT; // Reference to the RTT node for this segment
};
typedef std::map<std::pair<int, int>, 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;
};

View file

@ -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);

View file

@ -93,6 +93,8 @@
#include "../mwsound/constants.hpp"
#include "../mapextractor.hpp"
#include "actionteleport.hpp"
#include "cellstore.hpp"
#include "containerstore.hpp"
@ -249,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)
@ -261,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)
@ -1658,6 +1663,16 @@ namespace MWWorld
if (mGoToJail && !paused)
goToJail();
// Update map extraction if active
if (mMapExtractor)
{
mMapExtractor->update();
if (mMapExtractor->isExtractionComplete())
{
mMapExtractor.reset();
}
}
// Reset "traveling" flag - there was a frame to detect traveling.
mPlayerTraveling = false;
@ -3899,4 +3914,36 @@ namespace MWWorld
if (MWPhysics::Actor* const actor = mPhysics->getActor(ptr))
actor->setActive(value);
}
void World::extractWorldMap()
{
if (!mMapExtractor)
{
mMapExtractor = std::make_unique<OMW::MapExtractor>(
mWorldMapOutputPath, mLocalMapOutputPath, mOverwriteMaps, mRendering.get(), &mStore);
}
mMapExtractor->extractWorldMap();
}
void World::extractLocalMaps()
{
if (!mMapExtractor)
{
mMapExtractor = std::make_unique<OMW::MapExtractor>(
mWorldMapOutputPath, mLocalMapOutputPath, mOverwriteMaps, mRendering.get(), &mStore);
}
// Set LocalMap from WindowManager
if (auto* localMap = MWBase::Environment::get().getWindowManager()->getLocalMapRender())
{
mMapExtractor->setLocalMap(localMap);
}
const auto& activeCells = mWorldScene->getActiveCells();
std::vector<const MWWorld::CellStore*> cells(activeCells.begin(), activeCells.end());
mMapExtractor->extractLocalMaps(cells);
}
bool World::isMapExtractionActive() const
{
return mMapExtractor && !mMapExtractor->isExtractionComplete();
}
}

View file

@ -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<MWPhysics::PhysicsSystem> mPhysics;
std::unique_ptr<DetourNavigator::Navigator> mNavigator;
std::unique_ptr<MWRender::RenderingManager> mRendering;
std::unique_ptr<MWWorld::Scene> mWorldScene;
std::unique_ptr<MWWorld::WeatherManager> mWeatherManager;
std::unique_ptr<MWWorld::DateTimeManager> mTimeManager;
std::unique_ptr<ProjectileManager> mProjectileManager;
std::unique_ptr<MWWorld::Scene> mWorldScene;
std::unique_ptr<MWWorld::WeatherManager> mWeatherManager;
std::unique_ptr<MWWorld::DateTimeManager> mTimeManager;
std::unique_ptr<ProjectileManager> mProjectileManager;
std::unique_ptr<OMW::MapExtractor> mMapExtractor;
bool mSky;
bool mGodMode;
@ -118,6 +121,9 @@ namespace MWWorld
int mActivationDistanceOverride;
std::string mStartCell;
std::string mWorldMapOutputPath;
std::string mLocalMapOutputPath;
bool mOverwriteMaps;
float mSwimHeightScale;
@ -195,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<std::string>& contentFiles,
const std::vector<std::string>& groundcoverFiles, ToUTF8::Utf8Encoder* encoder,
@ -680,6 +686,14 @@ namespace MWWorld
DateTimeManager* getTimeManager() override { return mTimeManager.get(); }
void setActorActive(const MWWorld::Ptr& ptr, bool value) override;
std::string getWorldMapOutputPath() const override { return mWorldMapOutputPath; }
std::string getLocalMapOutputPath() const override { return mLocalMapOutputPath; }
bool getOverwriteMaps() const override { return mOverwriteMaps; }
void extractWorldMap() override;
void extractLocalMaps() override;
bool isMapExtractionActive() const override;
};
}

View file

@ -95,6 +95,15 @@ namespace OpenMW
addOption("random-seed", bpo::value<unsigned int>()->default_value(Misc::Rng::generateDefaultSeed()),
"seed value for random number generator");
addOption("world-map-output", bpo::value<std::string>()->default_value(""),
"directory to save world map texture (default: textures/advanced_world_map/custom)");
addOption("local-map-output", bpo::value<std::string>()->default_value(""),
"directory to save local map textures (default: textures/advanced_world_map/local)");
addOption("overwrite-maps", bpo::value<bool>()->implicit_value(true)->default_value(false),
"overwrite existing map files during extraction");
return desc;
}
}

View file

@ -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:

View file

@ -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;
}

View file

@ -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})

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,145 @@
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)
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
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)
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
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
::continue::
until i <= 0
if i <= 0 then
pl:sendEvent("builtin:map_extractor:updateMenu", {
line1 = "Map extraction complete.",
line2 = "",
line3 = "",
})
end
end
async:newUnsavableSimulationTimer(0.05, func)
end
async:newUnsavableSimulationTimer(0.1, function ()
local pl = world.players[1]
pl:sendEvent("builtin:map_extractor:updateMenu", {line1 = "Generating world map..."})
world.enableExtractionMode()
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)
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
processAndTeleport(true)
end)
end)
return {
eventHandlers = {
["builtin:map_extractor:teleport"] = function (pl)
processAndTeleport()
end,
}
}

View file

@ -0,0 +1,81 @@
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 function textLine()
return {
type = ui.TYPE.TextEdit,
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),
size = util.vector2(screenSize.x, 0),
multiline = true,
wordWrap = true,
readOnly = true,
},
}
end
local content = ui.content{
textLine(),
textLine(),
textLine(),
}
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
if data.line3 then
content[3].props.text = data.line3
end
menu:update()
end,
}
}

View file

@ -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,
}
}

View file

@ -221,4 +221,91 @@
-- @function [parent=#world] advanceTime
-- @param #number hours Number of hours to advance time
---
-- 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.
-- 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
-- @usage world.extractWorldMap() -- Use path from option or default
---
-- 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.
-- 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
-- @usage world.extractLocalMaps() -- Use path from option or default
---
-- Enable extraction mode for map generation.
-- This mode disables collision, AI, scripts, and enables god mode to facilitate map extraction.
-- Should be called before extractWorldMap or 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()
---
-- 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
---
-- 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