mirror of
https://github.com/OpenMW/openmw.git
synced 2026-01-24 15:00:55 +00:00
commit
a0eea54c21
30 changed files with 1620 additions and 136 deletions
15
CMakeSettings.json
Normal file
15
CMakeSettings.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
set(OPENMW_SOURCES
|
||||
engine.cpp
|
||||
options.cpp
|
||||
mapextractor.cpp
|
||||
)
|
||||
|
||||
set(OPENMW_RESOURCES
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
685
apps/openmw/mapextractor.cpp
Normal file
685
apps/openmw/mapextractor.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
103
apps/openmw/mapextractor.hpp
Normal file
103
apps/openmw/mapextractor.hpp
Normal 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
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
145
files/data/scripts/map_extractor/global.lua
Normal file
145
files/data/scripts/map_extractor/global.lua
Normal 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,
|
||||
}
|
||||
}
|
||||
81
files/data/scripts/map_extractor/menu.lua
Normal file
81
files/data/scripts/map_extractor/menu.lua
Normal 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,
|
||||
}
|
||||
}
|
||||
18
files/data/scripts/map_extractor/player.lua
Normal file
18
files/data/scripts/map_extractor/player.lua
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue