diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 538bfa11ec..b977d86a97 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -219,7 +219,7 @@ macOS11_Xcode12: CCACHE_SIZE: 3G variables: &engine-targets - targets: "openmw,openmw-iniimporter,openmw-launcher,openmw-wizard" + targets: "openmw,openmw-iniimporter,openmw-launcher,openmw-wizard,openmw-navmeshtool" package: "Engine" variables: &cs-targets diff --git a/CI/before_script.android.sh b/CI/before_script.android.sh index bdf7d4a244..43422f68c1 100755 --- a/CI/before_script.android.sh +++ b/CI/before_script.android.sh @@ -22,6 +22,7 @@ cmake \ -DBUILD_ESSIMPORTER=0 \ -DBUILD_OPENCS=0 \ -DBUILD_WIZARD=0 \ +-DBUILD_NAVMESHTOOL=OFF \ -DOPENMW_USE_SYSTEM_MYGUI=OFF \ -DOPENMW_USE_SYSTEM_SQLITE3=OFF \ .. diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index b9fed204e3..19d8338721 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -71,6 +71,7 @@ if [[ "${BUILD_TESTS_ONLY}" ]]; then -DBUILD_ESSIMPORTER=OFF \ -DBUILD_OPENCS=OFF \ -DBUILD_WIZARD=OFF \ + -DBUILD_NAVMESHTOOL=OFF \ -DBUILD_UNITTESTS=${BUILD_UNITTESTS} \ -DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \ -DGTEST_ROOT="${GOOGLETEST_DIR}" \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 644a7419a8..090e6c56e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ option(BUILD_DOCS "Build documentation." OFF ) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF) +option(BUILD_NAVMESHTOOL "Build navmesh tool" ON) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. @@ -603,6 +604,10 @@ if (BUILD_BENCHMARKS) add_subdirectory(apps/benchmarks) endif() +if (BUILD_NAVMESHTOOL) + add_subdirectory(apps/navmeshtool) +endif() + if (WIN32) if (MSVC) if (OPENMW_MP_BUILD) @@ -702,6 +707,10 @@ if (WIN32) if (BUILD_BENCHMARKS) set_target_properties(openmw_detournavigator_navmeshtilescache_benchmark PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() + + if (BUILD_NAVMESHTOOL) + set_target_properties(openmw-navmeshtool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + endif() endif(MSVC) # TODO: At some point release builds should not use the console but rather write to a log file @@ -944,6 +953,9 @@ elseif(NOT APPLE) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) + if(BUILD_NAVMESHTOOL) + install(PROGRAMS "${INSTALL_SOURCE}/openmw-navmeshtool" DESTINATION "${BINDIR}" ) + endif() # Install licenses INSTALL(FILES "files/mygui/DejaVuFontLicense.txt" DESTINATION "${LICDIR}" ) diff --git a/apps/navmeshtool/CMakeLists.txt b/apps/navmeshtool/CMakeLists.txt new file mode 100644 index 0000000000..7df049af97 --- /dev/null +++ b/apps/navmeshtool/CMakeLists.txt @@ -0,0 +1,22 @@ +set(NAVMESHTOOL + worldspacedata.cpp + navmesh.cpp + main.cpp +) +source_group(apps\\navmeshtool FILES ${NAVMESHTOOL}) + +openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL}) + +target_link_libraries(openmw-navmeshtool + ${Boost_PROGRAM_OPTIONS_LIBRARY} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions(--coverage) + target_link_libraries(openmw-navmeshtool gcov) +endif() + +if (WIN32) + install(TARGETS openmw-navmeshtool RUNTIME DESTINATION ".") +endif() diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp new file mode 100644 index 0000000000..3e867fcbc9 --- /dev/null +++ b/apps/navmeshtool/main.cpp @@ -0,0 +1,209 @@ +#include "worldspacedata.hpp" +#include "navmesh.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace NavMeshTool +{ + namespace + { + namespace bpo = boost::program_options; + + using StringsVector = std::vector; + + bpo::options_description makeOptionsDescription() + { + using Fallback::FallbackMap; + + bpo::options_description result; + + result.add_options() + ("help", "print help message") + + ("version", "print version information and quit") + + ("data", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") + + ("data-local", bpo::value()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""), + "set local data directory (highest priority)") + + ("fallback-archive", bpo::value()->default_value(StringsVector(), "fallback-archive") + ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") + + ("resources", bpo::value()->default_value(Files::MaybeQuotedPath(), "resources"), + "set resources directory") + + ("content", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") + + ("fs-strict", bpo::value()->implicit_value(true) + ->default_value(false), "strict file system handling (no case folding)") + + ("encoding", bpo::value()-> + default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default") + + ("fallback", bpo::value()->default_value(Fallback::FallbackMap(), "") + ->multitoken()->composing(), "fallback values") + + ("threads", bpo::value()->default_value(std::max(std::thread::hardware_concurrency() - 1, 1)), + "number of threads for parallel processing") + + ("process-interior-cells", bpo::value()->implicit_value(true) + ->default_value(false), "build navmesh for interior cells") + ; + + return result; + } + + void loadSettings(const Files::ConfigurationManager& config, Settings::Manager& settings) + { + const std::string localDefault = (config.getLocalPath() / "defaults.bin").string(); + const std::string globalDefault = (config.getGlobalPath() / "defaults.bin").string(); + + if (boost::filesystem::exists(localDefault)) + settings.loadDefault(localDefault); + else if (boost::filesystem::exists(globalDefault)) + settings.loadDefault(globalDefault); + else + throw std::runtime_error("No default settings file found! Make sure the file \"defaults.bin\" was properly installed."); + + const std::string settingsPath = (config.getUserConfigPath() / "settings.cfg").string(); + if (boost::filesystem::exists(settingsPath)) + settings.loadUser(settingsPath); + } + + int runNavMeshTool(int argc, char *argv[]) + { + bpo::options_description desc = makeOptionsDescription(); + + bpo::parsed_options options = bpo::command_line_parser(argc, argv) + .options(desc).allow_unregistered().run(); + bpo::variables_map variables; + + bpo::store(options, variables); + bpo::notify(variables); + + if (variables.find("help") != variables.end()) + { + getRawStdout() << desc << std::endl; + return 0; + } + + Files::ConfigurationManager config; + + bpo::variables_map composingVariables = Files::separateComposingVariables(variables, desc); + config.readConfiguration(variables, desc); + Files::mergeComposingVariables(variables, composingVariables, desc); + + const std::string encoding(variables["encoding"].as()); + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding)); + + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); + + auto local = variables["data-local"].as(); + if (!local.empty()) + dataDirs.push_back(std::move(local)); + + config.processPaths(dataDirs); + + const auto fsStrict = variables["fs-strict"].as(); + const auto resDir = variables["resources"].as(); + Version::Version v = Version::getOpenmwVersion(resDir.string()); + Log(Debug::Info) << v.describe(); + dataDirs.insert(dataDirs.begin(), resDir / "vfs"); + const auto fileCollections = Files::Collections(dataDirs, !fsStrict); + const auto archives = variables["fallback-archive"].as(); + const auto contentFiles = variables["content"].as(); + const std::size_t threadsNumber = variables["threads"].as(); + + if (threadsNumber < 1) + { + std::cerr << "Invalid threads number: " << threadsNumber << ", expected >= 1"; + return -1; + } + + const bool processInteriorCells = variables["process-interior-cells"].as(); + + Fallback::Map::init(variables["fallback"].as().mMap); + + VFS::Manager vfs(fsStrict); + + VFS::registerArchives(&vfs, fileCollections, archives, true); + + Settings::Manager settings; + loadSettings(config, settings); + + const osg::Vec3f agentHalfExtents = Settings::Manager::getVector3("default actor pathfind half extents", "Game"); + + DetourNavigator::NavMeshDb db((config.getUserDataPath() / "navmesh.db").string()); + + std::vector readers(contentFiles.size()); + EsmLoader::Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + const EsmLoader::EsmData esmData = EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder); + + Resource::ImageManager imageManager(&vfs); + Resource::NifFileManager nifFileManager(&vfs); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager); + Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager); + DetourNavigator::RecastGlobalAllocator::init(); + DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); + navigatorSettings.mRecast.mSwimHeightScale = EsmLoader::getGameSetting(esmData.mGameSettings, "fSwimHeightScale").getFloat(); + + WorldspaceData cellsData = gatherWorldspaceData(navigatorSettings, readers, vfs, bulletShapeManager, + esmData, processInteriorCells); + + generateAllNavMeshTiles(agentHalfExtents, navigatorSettings, threadsNumber, cellsData, std::move(db)); + + Log(Debug::Info) << "Done"; + + return 0; + } + } +} + +int main(int argc, char *argv[]) +{ + return wrapApplication(NavMeshTool::runNavMeshTool, argc, argv, "NavMeshTool"); +} diff --git a/apps/navmeshtool/navmesh.cpp b/apps/navmeshtool/navmesh.cpp new file mode 100644 index 0000000000..6b8150f1c0 --- /dev/null +++ b/apps/navmeshtool/navmesh.cpp @@ -0,0 +1,212 @@ +#include "navmesh.hpp" + +#include "worldspacedata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NavMeshTool +{ + namespace + { + using DetourNavigator::GenerateNavMeshTile; + using DetourNavigator::NavMeshDb; + using DetourNavigator::NavMeshTileInfo; + using DetourNavigator::PreparedNavMeshData; + using DetourNavigator::RecastMeshProvider; + using DetourNavigator::MeshSource; + using DetourNavigator::Settings; + using DetourNavigator::ShapeId; + using DetourNavigator::TileId; + using DetourNavigator::TilePosition; + using DetourNavigator::TileVersion; + using Sqlite3::Transaction; + + void logGeneratedTiles(std::size_t provided, std::size_t expected) + { + Log(Debug::Info) << provided << "/" << expected << " (" + << (static_cast(provided) / static_cast(expected) * 100) + << "%) navmesh tiles are generated"; + } + + struct LogGeneratedTiles + { + void operator()(std::size_t provided, std::size_t expected) const + { + logGeneratedTiles(provided, expected); + } + }; + + class NavMeshTileConsumer final : public DetourNavigator::NavMeshTileConsumer + { + public: + std::atomic_size_t mExpected {0}; + + explicit NavMeshTileConsumer(NavMeshDb db) + : mDb(std::move(db)) + , mTransaction(mDb.startTransaction()) + , mNextTileId(mDb.getMaxTileId() + 1) + , mNextShapeId(mDb.getMaxShapeId() + 1) + {} + + std::size_t getProvided() const { return mProvided.load(); } + + std::size_t getInserted() const { return mInserted.load(); } + + std::size_t getUpdated() const { return mUpdated.load(); } + + std::int64_t resolveMeshSource(const MeshSource& source) override + { + const std::lock_guard lock(mMutex); + return DetourNavigator::resolveMeshSource(mDb, source, mNextShapeId); + } + + std::optional find(const std::string& worldspace, const TilePosition &tilePosition, + const std::vector &input) override + { + std::optional result; + std::lock_guard lock(mMutex); + if (const auto tile = mDb.findTile(worldspace, tilePosition, input)) + { + NavMeshTileInfo info; + info.mTileId = tile->mTileId; + info.mVersion = tile->mVersion; + result.emplace(info); + } + return result; + } + + void ignore() override { report(); } + + void insert(const std::string& worldspace, const TilePosition& tilePosition, std::int64_t version, + const std::vector& input, PreparedNavMeshData& data) override + { + data.mUserId = static_cast(mNextTileId); + { + std::lock_guard lock(mMutex); + mDb.insertTile(mNextTileId, worldspace, tilePosition, TileVersion {version}, input, serialize(data)); + ++mNextTileId.t; + } + ++mInserted; + report(); + } + + void update(std::int64_t tileId, std::int64_t version, PreparedNavMeshData& data) override + { + data.mUserId = static_cast(tileId); + { + std::lock_guard lock(mMutex); + mDb.updateTile(TileId {tileId}, TileVersion {version}, serialize(data)); + } + ++mUpdated; + report(); + } + + void wait() + { + constexpr std::size_t tilesPerTransaction = 3000; + std::unique_lock lock(mMutex); + while (mProvided < mExpected) + { + mHasTile.wait(lock); + if (mProvided % tilesPerTransaction == 0) + { + mTransaction.commit(); + mTransaction = mDb.startTransaction(); + } + } + logGeneratedTiles(mProvided, mExpected); + } + + void commit() { mTransaction.commit(); } + + private: + std::atomic_size_t mProvided {0}; + std::atomic_size_t mInserted {0}; + std::atomic_size_t mUpdated {0}; + std::mutex mMutex; + NavMeshDb mDb; + Transaction mTransaction; + TileId mNextTileId; + std::condition_variable mHasTile; + Misc::ProgressReporter mReporter; + ShapeId mNextShapeId; + + void report() + { + mReporter(mProvided + 1, mExpected); + ++mProvided; + mHasTile.notify_one(); + } + }; + } + + void generateAllNavMeshTiles(const osg::Vec3f& agentHalfExtents, const Settings& settings, + const std::size_t threadsNumber, WorldspaceData& data, NavMeshDb&& db) + { + Log(Debug::Info) << "Generating navmesh tiles by " << threadsNumber << " parallel workers..."; + + SceneUtil::WorkQueue workQueue(threadsNumber); + auto navMeshTileConsumer = std::make_shared(std::move(db)); + std::size_t tiles = 0; + + for (const std::unique_ptr& input : data.mNavMeshInputs) + { + DetourNavigator::getTilesPositions( + Misc::Convert::toOsg(input->mAabb.m_min), Misc::Convert::toOsg(input->mAabb.m_max), settings.mRecast, + [&] (const TilePosition& tilePosition) + { + workQueue.addWorkItem(new GenerateNavMeshTile( + input->mWorldspace, + tilePosition, + RecastMeshProvider(input->mTileCachedRecastMeshManager), + agentHalfExtents, + settings, + navMeshTileConsumer + )); + + ++tiles; + }); + + navMeshTileConsumer->mExpected = tiles; + } + + navMeshTileConsumer->wait(); + navMeshTileConsumer->commit(); + + Log(Debug::Info) << "Generated navmesh for " << navMeshTileConsumer->getProvided() << " tiles, " + << navMeshTileConsumer->getInserted() << " are inserted and " + << navMeshTileConsumer->getUpdated() << " updated"; + } +} diff --git a/apps/navmeshtool/navmesh.hpp b/apps/navmeshtool/navmesh.hpp new file mode 100644 index 0000000000..725f0cd6a4 --- /dev/null +++ b/apps/navmeshtool/navmesh.hpp @@ -0,0 +1,23 @@ +#ifndef OPENMW_NAVMESHTOOL_NAVMESH_H +#define OPENMW_NAVMESHTOOL_NAVMESH_H + +#include + +#include +#include + +namespace DetourNavigator +{ + class NavMeshDb; + struct Settings; +} + +namespace NavMeshTool +{ + struct WorldspaceData; + + void generateAllNavMeshTiles(const osg::Vec3f& agentHalfExtents, const DetourNavigator::Settings& settings, + const std::size_t threadsNumber, WorldspaceData& cellsData, DetourNavigator::NavMeshDb&& db); +} + +#endif diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp new file mode 100644 index 0000000000..7c5aa06c83 --- /dev/null +++ b/apps/navmeshtool/worldspacedata.cpp @@ -0,0 +1,329 @@ +#include "worldspacedata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NavMeshTool +{ + namespace + { + using DetourNavigator::CollisionShape; + using DetourNavigator::HeightfieldPlane; + using DetourNavigator::HeightfieldShape; + using DetourNavigator::HeightfieldSurface; + using DetourNavigator::ObjectId; + using DetourNavigator::ObjectTransform; + + struct CellRef + { + ESM::RecNameInts mType; + ESM::RefNum mRefNum; + std::string mRefId; + float mScale; + ESM::Position mPos; + + CellRef(ESM::RecNameInts type, ESM::RefNum refNum, std::string&& refId, float scale, const ESM::Position& pos) + : mType(type), mRefNum(refNum), mRefId(std::move(refId)), mScale(scale), mPos(pos) {} + }; + + ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, std::string_view refId) + { + const auto it = std::lower_bound(esmData.mRefIdTypes.begin(), esmData.mRefIdTypes.end(), + refId, EsmLoader::LessById {}); + if (it == esmData.mRefIdTypes.end() || it->mId != refId) + return {}; + return it->mType; + } + + std::vector loadCellRefs(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, + std::vector& readers) + { + std::vector> cellRefs; + + for (std::size_t i = 0; i < cell.mContextList.size(); i++) + { + ESM::ESMReader& reader = readers[static_cast(cell.mContextList[i].index)]; + cell.restore(reader, static_cast(i)); + ESM::CellRef cellRef; + bool deleted = false; + while (ESM::Cell::getNextRef(reader, cellRef, deleted)) + { + Misc::StringUtils::lowerCaseInPlace(cellRef.mRefID); + const ESM::RecNameInts type = getType(esmData, cellRef.mRefID); + if (type == ESM::RecNameInts {}) + continue; + cellRefs.emplace_back(deleted, type, cellRef.mRefNum, std::move(cellRef.mRefID), + cellRef.mScale, cellRef.mPos); + } + } + + Log(Debug::Debug) << "Loaded " << cellRefs.size() << " cell refs"; + + const auto getKey = [] (const EsmLoader::Record& v) -> const ESM::RefNum& { return v.mValue.mRefNum; }; + std::vector result = prepareRecords(cellRefs, getKey); + + Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs"; + + return result; + } + + template + void forEachObject(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, std::vector& readers, + F&& f) + { + std::vector cellRefs = loadCellRefs(cell, esmData, readers); + + Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs"; + + for (CellRef& cellRef : cellRefs) + { + std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); + if (model.empty()) + continue; + + if (cellRef.mType != ESM::REC_STAT) + model = Misc::ResourceHelpers::correctActorModelPath(model, &vfs); + + osg::ref_ptr shape = [&] + { + try + { + return bulletShapeManager.getShape("meshes/" + model); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to load cell ref \"" << cellRef.mRefId << "\" model \"" << model << "\": " << e.what(); + return osg::ref_ptr(); + } + } (); + + if (shape == nullptr || shape->mCollisionShape == nullptr) + continue; + + osg::ref_ptr shapeInstance(new Resource::BulletShapeInstance(std::move(shape))); + + switch (cellRef.mType) + { + case ESM::REC_ACTI: + case ESM::REC_CONT: + case ESM::REC_DOOR: + case ESM::REC_STAT: + f(BulletObject(std::move(shapeInstance), cellRef.mPos, cellRef.mScale)); + break; + default: + break; + } + } + } + + struct GetXY + { + osg::Vec2i operator()(const ESM::Land& value) const { return osg::Vec2i(value.mX, value.mY); } + }; + + struct LessByXY + { + bool operator ()(const ESM::Land& lhs, const ESM::Land& rhs) const + { + return GetXY {}(lhs) < GetXY {}(rhs); + } + + bool operator ()(const ESM::Land& lhs, const osg::Vec2i& rhs) const + { + return GetXY {}(lhs) < rhs; + } + + bool operator ()(const osg::Vec2i& lhs, const ESM::Land& rhs) const + { + return lhs < GetXY {}(rhs); + } + }; + + btAABB getAabb(const osg::Vec2i& cellPosition, btScalar minHeight, btScalar maxHeight) + { + btAABB aabb; + aabb.m_min = btVector3( + static_cast(cellPosition.x() * ESM::Land::REAL_SIZE), + static_cast(cellPosition.y() * ESM::Land::REAL_SIZE), + minHeight + ); + aabb.m_min = btVector3( + static_cast((cellPosition.x() + 1) * ESM::Land::REAL_SIZE), + static_cast((cellPosition.y() + 1) * ESM::Land::REAL_SIZE), + maxHeight + ); + return aabb; + } + + void mergeOrAssign(const btAABB& aabb, btAABB& target, bool& initialized) + { + if (initialized) + return target.merge(aabb); + + target.m_min = aabb.m_min; + target.m_max = aabb.m_max; + initialized = true; + } + + std::tuple makeHeightfieldShape(const std::optional& land, + const osg::Vec2i& cellPosition, std::vector>& heightfields, + std::vector>& landDatas) + { + if (!land.has_value() || osg::Vec2i(land->mX, land->mY) != cellPosition + || (land->mDataTypes & ESM::Land::DATA_VHGT) == 0) + return {HeightfieldPlane {ESM::Land::DEFAULT_HEIGHT}, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT}; + + ESM::Land::LandData& landData = *landDatas.emplace_back(std::make_unique()); + land->loadData(ESM::Land::DATA_VHGT, &landData); + heightfields.emplace_back(std::vector(std::begin(landData.mHeights), std::end(landData.mHeights))); + HeightfieldSurface surface; + surface.mHeights = heightfields.back().data(); + surface.mMinHeight = landData.mMinHeight; + surface.mMaxHeight = landData.mMaxHeight; + surface.mSize = static_cast(ESM::Land::LAND_SIZE); + return {surface, landData.mMinHeight, landData.mMaxHeight}; + } + } + + WorldspaceNavMeshInput::WorldspaceNavMeshInput(std::string worldspace, const DetourNavigator::RecastSettings& settings) + : mWorldspace(std::move(worldspace)) + , mTileCachedRecastMeshManager(settings) + { + mAabb.m_min = btVector3(0, 0, 0); + mAabb.m_max = btVector3(0, 0, 0); + } + + WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, std::vector& readers, + const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + bool processInteriorCells) + { + Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells..."; + + std::map> navMeshInputs; + WorldspaceData data; + + std::size_t objectsCounter = 0; + + for (std::size_t i = 0; i < esmData.mCells.size(); ++i) + { + const ESM::Cell& cell = esmData.mCells[i]; + const bool exterior = cell.isExterior(); + + if (!exterior && !processInteriorCells) + { + Log(Debug::Info) << "Skipped " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; + continue; + } + + Log(Debug::Debug) << "Processing " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; + + const osg::Vec2i cellPosition(cell.mData.mX, cell.mData.mY); + const std::size_t cellObjectsBegin = data.mObjects.size(); + + WorldspaceNavMeshInput& navMeshInput = [&] () -> WorldspaceNavMeshInput& + { + auto it = navMeshInputs.find(cell.mCellId.mWorldspace); + if (it == navMeshInputs.end()) + { + it = navMeshInputs.emplace(cell.mCellId.mWorldspace, + std::make_unique(cell.mCellId.mWorldspace, settings.mRecast)).first; + } + return *it->second; + } (); + + if (exterior) + { + const auto it = std::lower_bound(esmData.mLands.begin(), esmData.mLands.end(), cellPosition, LessByXY {}); + const auto [heightfieldShape, minHeight, maxHeight] = makeHeightfieldShape( + it == esmData.mLands.end() ? std::optional() : *it, + cellPosition, data.mHeightfields, data.mLandData + ); + + mergeOrAssign(getAabb(cellPosition, minHeight, maxHeight), + navMeshInput.mAabb, navMeshInput.mAabbInitialized); + + navMeshInput.mTileCachedRecastMeshManager.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, heightfieldShape); + + navMeshInput.mTileCachedRecastMeshManager.addWater(cellPosition, ESM::Land::REAL_SIZE, -1); + } + else + { + if ((cell.mData.mFlags & ESM::Cell::HasWater) != 0) + navMeshInput.mTileCachedRecastMeshManager.addWater(cellPosition, std::numeric_limits::max(), cell.mWater); + } + + forEachObject(cell, esmData, vfs, bulletShapeManager, readers, + [&] (BulletObject object) + { + const btTransform& transform = object.getCollisionObject().getWorldTransform(); + const btAABB aabb = BulletHelpers::getAabb(*object.getCollisionObject().getCollisionShape(), transform); + mergeOrAssign(aabb, navMeshInput.mAabb, navMeshInput.mAabbInitialized); + if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) + navMeshInput.mAabb.merge(BulletHelpers::getAabb(*avoid, transform)); + + const ObjectId objectId(++objectsCounter); + const CollisionShape shape(object.getShapeInstance(), *object.getCollisionObject().getCollisionShape(), object.getObjectTransform()); + + navMeshInput.mTileCachedRecastMeshManager.addObject(objectId, shape, transform, DetourNavigator::AreaType_ground); + + if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) + { + const CollisionShape avoidShape(object.getShapeInstance(), *avoid, object.getObjectTransform()); + navMeshInput.mTileCachedRecastMeshManager.addObject(objectId, avoidShape, transform, DetourNavigator::AreaType_null); + } + + data.mObjects.emplace_back(std::move(object)); + }); + + Log(Debug::Info) << "Processed " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") " << cell.getDescription() + << " with " << (data.mObjects.size() - cellObjectsBegin) << " objects"; + } + + data.mNavMeshInputs.reserve(navMeshInputs.size()); + std::transform(navMeshInputs.begin(), navMeshInputs.end(), std::back_inserter(data.mNavMeshInputs), + [] (auto& v) { return std::move(v.second); }); + + Log(Debug::Info) << "Processed " << esmData.mCells.size() << " cells, added " + << data.mObjects.size() << " objects and " << data.mHeightfields.size() << " height fields"; + + return data; + } +} diff --git a/apps/navmeshtool/worldspacedata.hpp b/apps/navmeshtool/worldspacedata.hpp new file mode 100644 index 0000000000..3dccd5a8bc --- /dev/null +++ b/apps/navmeshtool/worldspacedata.hpp @@ -0,0 +1,97 @@ +#ifndef OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H +#define OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace ESM +{ + class ESMReader; +} + +namespace VFS +{ + class Manager; +} + +namespace Resource +{ + class BulletShapeManager; +} + +namespace EsmLoader +{ + struct EsmData; +} + +namespace DetourNavigator +{ + struct Settings; +} + +namespace NavMeshTool +{ + using DetourNavigator::TileCachedRecastMeshManager; + using DetourNavigator::ObjectTransform; + + struct WorldspaceNavMeshInput + { + std::string mWorldspace; + TileCachedRecastMeshManager mTileCachedRecastMeshManager; + btAABB mAabb; + bool mAabbInitialized = false; + + explicit WorldspaceNavMeshInput(std::string worldspace, const DetourNavigator::RecastSettings& settings); + }; + + class BulletObject + { + public: + BulletObject(osg::ref_ptr&& shapeInstance, const ESM::Position& position, + float localScaling) + : mShapeInstance(std::move(shapeInstance)) + , mObjectTransform {position, localScaling} + , mCollisionObject(BulletHelpers::makeCollisionObject( + mShapeInstance->mCollisionShape.get(), + Misc::Convert::toBullet(position.asVec3()), + Misc::Convert::toBullet(Misc::Convert::makeOsgQuat(position)) + )) + { + mShapeInstance->setLocalScaling(btVector3(localScaling, localScaling, localScaling)); + } + + const osg::ref_ptr& getShapeInstance() const noexcept { return mShapeInstance; } + const DetourNavigator::ObjectTransform& getObjectTransform() const noexcept { return mObjectTransform; } + btCollisionObject& getCollisionObject() const noexcept { return *mCollisionObject; } + + private: + osg::ref_ptr mShapeInstance; + DetourNavigator::ObjectTransform mObjectTransform; + std::unique_ptr mCollisionObject; + }; + + struct WorldspaceData + { + std::vector> mNavMeshInputs; + std::vector mObjects; + std::vector> mLandData; + std::vector> mHeightfields; + }; + + WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, std::vector& readers, + const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + bool processInteriorCells); +} + +#endif diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 57e05b8511..6a93eaa397 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -41,6 +41,8 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/recastmeshobject.cpp detournavigator/navmeshtilescache.cpp detournavigator/tilecachedrecastmeshmanager.cpp + detournavigator/navmeshdb.cpp + detournavigator/serialization.cpp serialization/binaryreader.cpp serialization/binarywriter.cpp diff --git a/apps/openmw_test_suite/detournavigator/generate.hpp b/apps/openmw_test_suite/detournavigator/generate.hpp new file mode 100644 index 0000000000..52d04495a7 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/generate.hpp @@ -0,0 +1,51 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H + +#include +#include +#include +#include + +namespace DetourNavigator +{ + namespace Tests + { + template + inline auto generateValue(T& value, Random& random) + -> std::enable_if_t= 2> + { + using Distribution = std::conditional_t< + std::is_floating_point_v, + std::uniform_real_distribution, + std::uniform_int_distribution + >; + Distribution distribution(std::numeric_limits::min(), std::numeric_limits::max()); + value = distribution(random); + } + + template + inline auto generateValue(T& value, Random& random) + -> std::enable_if_t + { + unsigned short v; + generateValue(v, random); + value = static_cast(v % 256); + } + + template + inline void generateValue(unsigned char& value, Random& random) + { + unsigned short v; + generateValue(v, random); + value = static_cast(v % 256); + } + + template + inline void generateRange(I begin, I end, Random& random) + { + std::for_each(begin, end, [&] (auto& v) { generateValue(v, random); }); + } + } +} + +#endif diff --git a/apps/openmw_test_suite/detournavigator/navmeshdb.cpp b/apps/openmw_test_suite/detournavigator/navmeshdb.cpp new file mode 100644 index 0000000000..feadc2f59d --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/navmeshdb.cpp @@ -0,0 +1,112 @@ +#include "generate.hpp" + +#include +#include + +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + struct Tile + { + std::string mWorldspace; + TilePosition mTilePosition; + std::vector mInput; + std::vector mData; + }; + + struct DetourNavigatorNavMeshDbTest : Test + { + NavMeshDb mDb {":memory:"}; + std::minstd_rand mRandom; + + std::vector generateData() + { + std::vector data(32); + generateRange(data.begin(), data.end(), mRandom); + return data; + } + + Tile insertTile(TileId tileId, TileVersion version) + { + std::string worldspace = "sys::default"; + const TilePosition tilePosition {3, 4}; + std::vector input = generateData(); + std::vector data = generateData(); + EXPECT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + return {std::move(worldspace), tilePosition, std::move(input), std::move(data)}; + } + }; + + TEST_F(DetourNavigatorNavMeshDbTest, get_max_tile_id_for_empty_db_should_return_zero) + { + EXPECT_EQ(mDb.getMaxTileId(), TileId {0}); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_be_found_by_key) + { + const TileId tileId {146}; + const TileVersion version {1}; + const auto [worldspace, tilePosition, input, data] = insertTile(tileId, version); + const auto result = mDb.findTile(worldspace, tilePosition, input); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->mTileId, tileId); + EXPECT_EQ(result->mVersion, version); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_change_max_tile_id) + { + insertTile(TileId {53}, TileVersion {1}); + EXPECT_EQ(mDb.getMaxTileId(), TileId {53}); + } + + TEST_F(DetourNavigatorNavMeshDbTest, updated_tile_should_change_data) + { + const TileId tileId {13}; + const TileVersion version {1}; + auto [worldspace, tilePosition, input, data] = insertTile(tileId, version); + generateRange(data.begin(), data.end(), mRandom); + ASSERT_EQ(mDb.updateTile(tileId, version, data), 1); + const auto row = mDb.getTileData(worldspace, tilePosition, input); + ASSERT_TRUE(row.has_value()); + EXPECT_EQ(row->mTileId, tileId); + EXPECT_EQ(row->mVersion, version); + ASSERT_FALSE(row->mData.empty()); + EXPECT_EQ(row->mData, data); + } + + TEST_F(DetourNavigatorNavMeshDbTest, on_inserted_duplicate_should_throw_exception) + { + const TileId tileId {53}; + const TileVersion version {1}; + const std::string worldspace = "sys::default"; + const TilePosition tilePosition {3, 4}; + const std::vector input = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_duplicate_leaves_db_in_correct_state) + { + const TileId tileId {53}; + const TileVersion version {1}; + const std::string worldspace = "sys::default"; + const TilePosition tilePosition {3, 4}; + const std::vector input = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error); + EXPECT_NO_THROW(insertTile(TileId {54}, version)); + } +} diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp index 2a7c5ac5a1..b47ae98233 100644 --- a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp +++ b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp @@ -1,4 +1,5 @@ #include "operators.hpp" +#include "generate.hpp" #include #include @@ -21,6 +22,7 @@ namespace { using namespace testing; using namespace DetourNavigator; + using namespace DetourNavigator::Tests; void* permRecastAlloc(int size) { @@ -30,14 +32,15 @@ namespace return result; } - template - void generate(T*& values, int size) + template + void generateRecastArray(T*& values, int size, Random& random) { values = static_cast(permRecastAlloc(size * sizeof(T))); - std::generate_n(values, static_cast(size), [] { return static_cast(std::rand()); }); + generateRange(values, values + static_cast(size), random); } - void generate(rcPolyMesh& value, int size) + template + void generate(rcPolyMesh& value, int size, Random& random) { value.nverts = size; value.maxpolys = size; @@ -45,40 +48,43 @@ namespace value.npolys = size; rcVcopy(value.bmin, osg::Vec3f(-1, -2, -3).ptr()); rcVcopy(value.bmax, osg::Vec3f(3, 2, 1).ptr()); - value.cs = 1.0f / (std::rand() % 999 + 1); - value.ch = 1.0f / (std::rand() % 999 + 1); - value.borderSize = std::rand(); - value.maxEdgeError = 1.0f / (std::rand() % 999 + 1); - generate(value.verts, getVertsLength(value)); - generate(value.polys, getPolysLength(value)); - generate(value.regs, getRegsLength(value)); - generate(value.flags, getFlagsLength(value)); - generate(value.areas, getAreasLength(value)); + generateValue(value.cs, random); + generateValue(value.ch, random); + generateValue(value.borderSize, random); + generateValue(value.maxEdgeError, random); + generateRecastArray(value.verts, getVertsLength(value), random); + generateRecastArray(value.polys, getPolysLength(value), random); + generateRecastArray(value.regs, getRegsLength(value), random); + generateRecastArray(value.flags, getFlagsLength(value), random); + generateRecastArray(value.areas, getAreasLength(value), random); } - void generate(rcPolyMeshDetail& value, int size) + template + void generate(rcPolyMeshDetail& value, int size, Random& random) { value.nmeshes = size; value.nverts = size; value.ntris = size; - generate(value.meshes, getMeshesLength(value)); - generate(value.verts, getVertsLength(value)); - generate(value.tris, getTrisLength(value)); + generateRecastArray(value.meshes, getMeshesLength(value), random); + generateRecastArray(value.verts, getVertsLength(value), random); + generateRecastArray(value.tris, getTrisLength(value), random); } - void generate(PreparedNavMeshData& value, int size) + template + void generate(PreparedNavMeshData& value, int size, Random& random) { - value.mUserId = std::rand(); - value.mCellHeight = 1.0f / (std::rand() % 999 + 1); - value.mCellSize = 1.0f / (std::rand() % 999 + 1); - generate(value.mPolyMesh, size); - generate(value.mPolyMeshDetail, size); + generateValue(value.mUserId, random); + generateValue(value.mCellHeight, random); + generateValue(value.mCellSize, random); + generate(value.mPolyMesh, size, random); + generate(value.mPolyMeshDetail, size, random); } std::unique_ptr makePeparedNavMeshData(int size) { + std::minstd_rand random; auto result = std::make_unique(); - generate(*result, size); + generate(*result, size, random); return result; } diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index b145f61332..be3efd1b9f 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -204,6 +204,9 @@ add_component_dir(detournavigator navmeshcacheitem navigatorutils generatenavmeshtile + navmeshdb + serialization + navmeshdbutils ) add_component_dir(loadinglistener diff --git a/components/detournavigator/cachedrecastmeshmanager.cpp b/components/detournavigator/cachedrecastmeshmanager.cpp index a3c7f4b172..e350c5591f 100644 --- a/components/detournavigator/cachedrecastmeshmanager.cpp +++ b/components/detournavigator/cachedrecastmeshmanager.cpp @@ -84,6 +84,11 @@ namespace DetourNavigator return *mCached.lockConst(); } + std::shared_ptr CachedRecastMeshManager::getNewMesh() const + { + return mImpl.getMesh(); + } + bool CachedRecastMeshManager::isEmpty() const { return mImpl.isEmpty(); diff --git a/components/detournavigator/cachedrecastmeshmanager.hpp b/components/detournavigator/cachedrecastmeshmanager.hpp index eb18519891..b92d39efa4 100644 --- a/components/detournavigator/cachedrecastmeshmanager.hpp +++ b/components/detournavigator/cachedrecastmeshmanager.hpp @@ -35,6 +35,8 @@ namespace DetourNavigator std::shared_ptr getCachedMesh() const; + std::shared_ptr getNewMesh() const; + bool isEmpty() const; void reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion); diff --git a/components/detournavigator/dbrefgeometryobject.hpp b/components/detournavigator/dbrefgeometryobject.hpp new file mode 100644 index 0000000000..2be44ca175 --- /dev/null +++ b/components/detournavigator/dbrefgeometryobject.hpp @@ -0,0 +1,46 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H + +#include "objecttransform.hpp" +#include "recastmesh.hpp" + +#include +#include +#include +#include + +namespace DetourNavigator +{ + struct DbRefGeometryObject + { + std::int64_t mShapeId; + ObjectTransform mObjectTransform; + + friend inline auto tie(const DbRefGeometryObject& v) + { + return std::tie(v.mShapeId, v.mObjectTransform); + } + + friend inline bool operator<(const DbRefGeometryObject& l, const DbRefGeometryObject& r) + { + return tie(l) < tie(r); + } + }; + + template + inline std::vector makeDbRefGeometryObjects(const std::vector& meshSources, + ResolveMeshSource&& resolveMeshSource) + { + std::vector result; + result.reserve(meshSources.size()); + std::transform(meshSources.begin(), meshSources.end(), std::back_inserter(result), + [&] (const MeshSource& meshSource) + { + return DbRefGeometryObject {resolveMeshSource(meshSource), meshSource.mObjectTransform}; + }); + std::sort(result.begin(), result.end()); + return result; + } +} + +#endif diff --git a/components/detournavigator/generatenavmeshtile.cpp b/components/detournavigator/generatenavmeshtile.cpp new file mode 100644 index 0000000000..9dc8e70383 --- /dev/null +++ b/components/detournavigator/generatenavmeshtile.cpp @@ -0,0 +1,95 @@ +#include "generatenavmeshtile.hpp" + +#include "dbrefgeometryobject.hpp" +#include "makenavmesh.hpp" +#include "offmeshconnectionsmanager.hpp" +#include "preparednavmeshdata.hpp" +#include "serialization.hpp" +#include "settings.hpp" +#include "tilecachedrecastmeshmanager.hpp" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace DetourNavigator +{ + namespace + { + struct Ignore + { + std::shared_ptr mConsumer; + + ~Ignore() noexcept + { + if (mConsumer != nullptr) + mConsumer->ignore(); + } + }; + } + + GenerateNavMeshTile::GenerateNavMeshTile(std::string worldspace, const TilePosition& tilePosition, + RecastMeshProvider recastMeshProvider, const osg::Vec3f& agentHalfExtents, + const DetourNavigator::Settings& settings, std::weak_ptr consumer) + : mWorldspace(std::move(worldspace)) + , mTilePosition(tilePosition) + , mRecastMeshProvider(recastMeshProvider) + , mAgentHalfExtents(agentHalfExtents) + , mSettings(settings) + , mConsumer(std::move(consumer)) {} + + void GenerateNavMeshTile::doWork() + { + impl(); + } + + void GenerateNavMeshTile::impl() noexcept + { + const auto consumer = mConsumer.lock(); + + if (consumer == nullptr) + return; + + try + { + Ignore ignore {consumer}; + + const std::shared_ptr recastMesh = mRecastMeshProvider.getMesh(mTilePosition); + + if (recastMesh == nullptr || isEmpty(*recastMesh)) + return; + + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&] (const MeshSource& v) { return consumer->resolveMeshSource(v); }); + std::vector input = serialize(mSettings.mRecast, *recastMesh, objects); + const std::optional info = consumer->find(mWorldspace, mTilePosition, input); + + if (info.has_value() && info->mVersion == mSettings.mNavMeshVersion) + return; + + const auto data = prepareNavMeshTileData(*recastMesh, mTilePosition, mAgentHalfExtents, mSettings.mRecast); + + if (data == nullptr) + return; + + if (info.has_value()) + consumer->update(info->mTileId, mSettings.mNavMeshVersion, *data); + else + consumer->insert(mWorldspace, mTilePosition, mSettings.mNavMeshVersion, input, *data); + + ignore.mConsumer = nullptr; + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to generate navmesh for worldspace \"" << mWorldspace + << "\" tile " << mTilePosition << ": " << e.what(); + } + } +} diff --git a/components/detournavigator/generatenavmeshtile.hpp b/components/detournavigator/generatenavmeshtile.hpp new file mode 100644 index 0000000000..511b8dfb8f --- /dev/null +++ b/components/detournavigator/generatenavmeshtile.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H + +#include "recastmeshprovider.hpp" +#include "tileposition.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace DetourNavigator +{ + class OffMeshConnectionsManager; + class RecastMesh; + struct NavMeshTileConsumer; + struct OffMeshConnection; + struct PreparedNavMeshData; + struct Settings; + + struct NavMeshTileInfo + { + std::int64_t mTileId; + std::int64_t mVersion; + }; + + struct NavMeshTileConsumer + { + virtual ~NavMeshTileConsumer() = default; + + virtual std::int64_t resolveMeshSource(const MeshSource& source) = 0; + + virtual std::optional find(const std::string& worldspace, const TilePosition& tilePosition, + const std::vector& input) = 0; + + virtual void ignore() = 0; + + virtual void insert(const std::string& worldspace, const TilePosition& tilePosition, + std::int64_t version, const std::vector& input, PreparedNavMeshData& data) = 0; + + virtual void update(std::int64_t tileId, std::int64_t version, PreparedNavMeshData& data) = 0; + }; + + class GenerateNavMeshTile final : public SceneUtil::WorkItem + { + public: + GenerateNavMeshTile(std::string worldspace, const TilePosition& tilePosition, + RecastMeshProvider recastMeshProvider, const osg::Vec3f& agentHalfExtents, const Settings& settings, + std::weak_ptr consumer); + + void doWork() final; + + private: + const std::string mWorldspace; + const TilePosition mTilePosition; + const RecastMeshProvider mRecastMeshProvider; + const osg::Vec3f mAgentHalfExtents; + const Settings& mSettings; + std::weak_ptr mConsumer; + + inline void impl() noexcept; + }; +} + +#endif diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 217e2b565e..4bfffb22ab 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -114,6 +114,30 @@ namespace return waterLevel - settings.mSwimHeightScale * agentHalfExtentsZ - agentHalfExtentsZ;; } + struct RecastParams + { + float mSampleDist = 0; + float mSampleMaxError = 0; + int mMaxEdgeLen = 0; + int mWalkableClimb = 0; + int mWalkableHeight = 0; + int mWalkableRadius = 0; + }; + + RecastParams makeRecastParams(const RecastSettings& settings, const osg::Vec3f& agentHalfExtents) + { + RecastParams result; + + result.mWalkableHeight = static_cast(std::ceil(getHeight(settings, agentHalfExtents) / settings.mCellHeight)); + result.mWalkableClimb = static_cast(std::floor(getMaxClimb(settings) / settings.mCellHeight)); + result.mWalkableRadius = static_cast(std::ceil(getRadius(settings, agentHalfExtents) / settings.mCellSize)); + result.mMaxEdgeLen = static_cast(std::round(static_cast(settings.mMaxEdgeLen) / settings.mCellSize)); + result.mSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : settings.mCellSize * settings.mDetailSampleDist; + result.mSampleMaxError = settings.mCellHeight * settings.mDetailSampleMaxError; + + return result; + } + void initHeightfield(rcContext& context, const TilePosition& tilePosition, float minZ, float maxZ, const RecastSettings& settings, rcHeightfield& solid) { @@ -399,27 +423,13 @@ namespace namespace DetourNavigator { - RecastParams makeRecastParams(const RecastSettings& settings, const osg::Vec3f& agentHalfExtents) - { - RecastParams result; - - result.mWalkableHeight = static_cast(std::ceil(getHeight(settings, agentHalfExtents) / settings.mCellHeight)); - result.mWalkableClimb = static_cast(std::floor(getMaxClimb(settings) / settings.mCellHeight)); - result.mWalkableRadius = static_cast(std::ceil(getRadius(settings, agentHalfExtents) / settings.mCellSize)); - result.mMaxEdgeLen = static_cast(std::round(static_cast(settings.mMaxEdgeLen) / settings.mCellSize)); - result.mSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : settings.mCellSize * settings.mDetailSampleDist; - result.mSampleMaxError = settings.mCellHeight * settings.mDetailSampleMaxError; - - return result; - } - std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents, const RecastSettings& settings) { - const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentHalfExtents, settings); - rcContext context; + const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentHalfExtents, settings); + rcHeightfield solid; initHeightfield(context, tilePosition, toNavMeshCoordinates(settings, minZ), toNavMeshCoordinates(settings, maxZ), settings, solid); @@ -549,8 +559,7 @@ namespace DetourNavigator return navMeshCacheItem->lock()->removeTile(changedTile); } - if (recastMesh->getMesh().getIndices().empty() && recastMesh->getWater().empty() - && recastMesh->getHeightfields().empty() && recastMesh->getFlatHeightfields().empty()) + if (isEmpty(*recastMesh)) { Log(Debug::Debug) << "Ignore add tile: recastMesh is empty"; return navMeshCacheItem->lock()->removeTile(changedTile); diff --git a/components/detournavigator/makenavmesh.hpp b/components/detournavigator/makenavmesh.hpp index 989565d4bf..fccf70baad 100644 --- a/components/detournavigator/makenavmesh.hpp +++ b/components/detournavigator/makenavmesh.hpp @@ -14,6 +14,7 @@ #include class dtNavMesh; +struct rcConfig; namespace DetourNavigator { @@ -22,16 +23,6 @@ namespace DetourNavigator struct PreparedNavMeshData; struct NavMeshData; - struct RecastParams - { - float mSampleDist = 0; - float mSampleMaxError = 0; - int mMaxEdgeLen = 0; - int mWalkableClimb = 0; - int mWalkableHeight = 0; - int mWalkableRadius = 0; - }; - inline float getLength(const osg::Vec2i& value) { return std::sqrt(float(osg::square(value.x()) + osg::square(value.y()))); @@ -48,10 +39,16 @@ namespace DetourNavigator return expectedTilesCount <= maxTiles; } - RecastParams makeRecastParams(const RecastSettings& settings, const osg::Vec3f& agentHalfExtents); + inline bool isEmpty(const RecastMesh& recastMesh) + { + return recastMesh.getMesh().getIndices().empty() + && recastMesh.getWater().empty() + && recastMesh.getHeightfields().empty() + && recastMesh.getFlatHeightfields().empty(); + } - std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, const TilePosition& tile, - const Bounds& bounds, const osg::Vec3f& agentHalfExtents, const Settings& settings); + std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, + const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents, const RecastSettings& settings); NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data, const std::vector& offMeshConnections, const osg::Vec3f& agentHalfExtents, diff --git a/components/detournavigator/navmeshdb.cpp b/components/detournavigator/navmeshdb.cpp new file mode 100644 index 0000000000..ebff250ee0 --- /dev/null +++ b/components/detournavigator/navmeshdb.cpp @@ -0,0 +1,296 @@ +#include "navmeshdb.hpp" + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +namespace DetourNavigator +{ + namespace + { + constexpr const char schema[] = R"( + BEGIN TRANSACTION; + + CREATE TABLE IF NOT EXISTS tiles ( + tile_id INTEGER PRIMARY KEY, + revision INTEGER NOT NULL DEFAULT 1, + worldspace TEXT NOT NULL, + tile_position_x INTEGER NOT NULL, + tile_position_y INTEGER NOT NULL, + version INTEGER NOT NULL, + input BLOB, + data BLOB + ); + + CREATE UNIQUE INDEX IF NOT EXISTS index_unique_tiles_by_worldspace_and_tile_position_and_input + ON tiles (worldspace, tile_position_x, tile_position_y, input); + + CREATE TABLE IF NOT EXISTS shapes ( + shape_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + type INTEGER NOT NULL, + hash BLOB NOT NULL + ); + + CREATE UNIQUE INDEX IF NOT EXISTS index_unique_shapes_by_name_and_type_and_hash + ON shapes (name, type, hash); + + COMMIT; + )"; + + constexpr std::string_view getMaxTileIdQuery = R"( + SELECT max(tile_id) FROM tiles + )"; + + constexpr std::string_view findTileQuery = R"( + SELECT tile_id, version + FROM tiles + WHERE worldspace = :worldspace + AND tile_position_x = :tile_position_x + AND tile_position_y = :tile_position_y + AND input = :input + )"; + + constexpr std::string_view getTileDataQuery = R"( + SELECT tile_id, version, data + FROM tiles + WHERE worldspace = :worldspace + AND tile_position_x = :tile_position_x + AND tile_position_y = :tile_position_y + AND input = :input + )"; + + constexpr std::string_view insertTileQuery = R"( + INSERT INTO tiles ( tile_id, worldspace, version, tile_position_x, tile_position_y, input, data) + VALUES (:tile_id, :worldspace, :version, :tile_position_x, :tile_position_y, :input, :data) + )"; + + constexpr std::string_view updateTileQuery = R"( + UPDATE tiles + SET version = :version, + data = :data, + revision = revision + 1 + WHERE tile_id = :tile_id + )"; + + constexpr std::string_view getMaxShapeIdQuery = R"( + SELECT max(shape_id) FROM shapes + )"; + + constexpr std::string_view findShapeIdQuery = R"( + SELECT shape_id + FROM shapes + WHERE name = :name + AND type = :type + AND hash = :hash + )"; + + constexpr std::string_view insertShapeQuery = R"( + INSERT INTO shapes ( shape_id, name, type, hash) + VALUES (:shape_id, :name, :type, :hash) + )"; + } + + std::ostream& operator<<(std::ostream& stream, ShapeType value) + { + switch (value) + { + case ShapeType::Collision: return stream << "collision"; + case ShapeType::Avoid: return stream << "avoid"; + } + return stream << "unknown shape type (" << static_cast>(value) << ")"; + } + + NavMeshDb::NavMeshDb(std::string_view path) + : mDb(Sqlite3::makeDb(path, schema)) + , mGetMaxTileId(*mDb, DbQueries::GetMaxTileId {}) + , mFindTile(*mDb, DbQueries::FindTile {}) + , mGetTileData(*mDb, DbQueries::GetTileData {}) + , mInsertTile(*mDb, DbQueries::InsertTile {}) + , mUpdateTile(*mDb, DbQueries::UpdateTile {}) + , mGetMaxShapeId(*mDb, DbQueries::GetMaxShapeId {}) + , mFindShapeId(*mDb, DbQueries::FindShapeId {}) + , mInsertShape(*mDb, DbQueries::InsertShape {}) + { + } + + Sqlite3::Transaction NavMeshDb::startTransaction() + { + return Sqlite3::Transaction(*mDb); + } + + TileId NavMeshDb::getMaxTileId() + { + TileId tileId {0}; + request(*mDb, mGetMaxTileId, &tileId, 1); + return tileId; + } + + std::optional NavMeshDb::findTile(const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + Tile result; + auto row = std::tie(result.mTileId, result.mVersion); + const std::vector compressedInput = Misc::compress(input); + if (&row == request(*mDb, mFindTile, &row, 1, worldspace, tilePosition, compressedInput)) + return {}; + return result; + } + + std::optional NavMeshDb::getTileData(const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + TileData result; + auto row = std::tie(result.mTileId, result.mVersion, result.mData); + const std::vector compressedInput = Misc::compress(input); + if (&row == request(*mDb, mGetTileData, &row, 1, worldspace, tilePosition, compressedInput)) + return {}; + result.mData = Misc::decompress(result.mData); + return result; + } + + int NavMeshDb::insertTile(TileId tileId, const std::string& worldspace, const TilePosition& tilePosition, + TileVersion version, const std::vector& input, const std::vector& data) + { + const std::vector compressedInput = Misc::compress(input); + const std::vector compressedData = Misc::compress(data); + return execute(*mDb, mInsertTile, tileId, worldspace, tilePosition, version, compressedInput, compressedData); + } + + int NavMeshDb::updateTile(TileId tileId, TileVersion version, const std::vector& data) + { + const std::vector compressedData = Misc::compress(data); + return execute(*mDb, mUpdateTile, tileId, version, compressedData); + } + + ShapeId NavMeshDb::getMaxShapeId() + { + ShapeId shapeId {0}; + request(*mDb, mGetMaxShapeId, &shapeId, 1); + return shapeId; + } + + std::optional NavMeshDb::findShapeId(const std::string& name, ShapeType type, + const Sqlite3::ConstBlob& hash) + { + ShapeId shapeId; + if (&shapeId == request(*mDb, mFindShapeId, &shapeId, 1, name, type, hash)) + return {}; + return shapeId; + } + + int NavMeshDb::insertShape(ShapeId shapeId, const std::string& name, ShapeType type, + const Sqlite3::ConstBlob& hash) + { + return execute(*mDb, mInsertShape, shapeId, name, type, hash); + } + + namespace DbQueries + { + std::string_view GetMaxTileId::text() noexcept + { + return getMaxTileIdQuery; + } + + std::string_view FindTile::text() noexcept + { + return findTileQuery; + } + + void FindTile::bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + Sqlite3::bindParameter(db, statement, ":input", input); + } + + std::string_view GetTileData::text() noexcept + { + return getTileDataQuery; + } + + void GetTileData::bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input) + { + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + Sqlite3::bindParameter(db, statement, ":input", input); + } + + std::string_view InsertTile::text() noexcept + { + return insertTileQuery; + } + + void InsertTile::bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, const std::string& worldspace, + const TilePosition& tilePosition, TileVersion version, const std::vector& input, + const std::vector& data) + { + Sqlite3::bindParameter(db, statement, ":tile_id", tileId); + Sqlite3::bindParameter(db, statement, ":worldspace", worldspace); + Sqlite3::bindParameter(db, statement, ":tile_position_x", tilePosition.x()); + Sqlite3::bindParameter(db, statement, ":tile_position_y", tilePosition.y()); + Sqlite3::bindParameter(db, statement, ":version", version); + Sqlite3::bindParameter(db, statement, ":input", input); + Sqlite3::bindParameter(db, statement, ":data", data); + } + + std::string_view UpdateTile::text() noexcept + { + return updateTileQuery; + } + + void UpdateTile::bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, TileVersion version, + const std::vector& data) + { + Sqlite3::bindParameter(db, statement, ":tile_id", tileId); + Sqlite3::bindParameter(db, statement, ":version", version); + Sqlite3::bindParameter(db, statement, ":data", data); + } + + std::string_view GetMaxShapeId::text() noexcept + { + return getMaxShapeIdQuery; + } + + std::string_view FindShapeId::text() noexcept + { + return findShapeIdQuery; + } + + void FindShapeId::bind(sqlite3& db, sqlite3_stmt& statement, const std::string& name, + ShapeType type, const Sqlite3::ConstBlob& hash) + { + Sqlite3::bindParameter(db, statement, ":name", name); + Sqlite3::bindParameter(db, statement, ":type", static_cast(type)); + Sqlite3::bindParameter(db, statement, ":hash", hash); + } + + std::string_view InsertShape::text() noexcept + { + return insertShapeQuery; + } + + void InsertShape::bind(sqlite3& db, sqlite3_stmt& statement, ShapeId shapeId, const std::string& name, + ShapeType type, const Sqlite3::ConstBlob& hash) + { + Sqlite3::bindParameter(db, statement, ":shape_id", shapeId); + Sqlite3::bindParameter(db, statement, ":name", name); + Sqlite3::bindParameter(db, statement, ":type", static_cast(type)); + Sqlite3::bindParameter(db, statement, ":hash", hash); + } + } +} diff --git a/components/detournavigator/navmeshdb.hpp b/components/detournavigator/navmeshdb.hpp new file mode 100644 index 0000000000..636f1de000 --- /dev/null +++ b/components/detournavigator/navmeshdb.hpp @@ -0,0 +1,153 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H + +#include "tileposition.hpp" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct sqlite3; +struct sqlite3_stmt; + +namespace DetourNavigator +{ + BOOST_STRONG_TYPEDEF(std::int64_t, TileId) + BOOST_STRONG_TYPEDEF(std::int64_t, TileRevision) + BOOST_STRONG_TYPEDEF(std::int64_t, TileVersion) + BOOST_STRONG_TYPEDEF(std::int64_t, ShapeId) + + struct Tile + { + TileId mTileId; + TileVersion mVersion; + }; + + struct TileData + { + TileId mTileId; + TileVersion mVersion; + std::vector mData; + }; + + enum class ShapeType + { + Collision = 1, + Avoid = 2, + }; + + std::ostream& operator<<(std::ostream& stream, ShapeType value); + + namespace DbQueries + { + struct GetMaxTileId + { + static std::string_view text() noexcept; + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct FindTile + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input); + }; + + struct GetTileData + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input); + }; + + struct InsertTile + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, const std::string& worldspace, + const TilePosition& tilePosition, TileVersion version, const std::vector& input, + const std::vector& data); + }; + + struct UpdateTile + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, TileVersion version, + const std::vector& data); + }; + + struct GetMaxShapeId + { + static std::string_view text() noexcept; + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct FindShapeId + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, const std::string& name, + ShapeType type, const Sqlite3::ConstBlob& hash); + }; + + struct InsertShape + { + static std::string_view text() noexcept; + static void bind(sqlite3& db, sqlite3_stmt& statement, ShapeId shapeId, const std::string& name, + ShapeType type, const Sqlite3::ConstBlob& hash); + }; + } + + class NavMeshDb + { + public: + explicit NavMeshDb(std::string_view path); + + Sqlite3::Transaction startTransaction(); + + TileId getMaxTileId(); + + std::optional findTile(const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input); + + std::optional getTileData(const std::string& worldspace, + const TilePosition& tilePosition, const std::vector& input); + + int insertTile(TileId tileId, const std::string& worldspace, const TilePosition& tilePosition, + TileVersion version, const std::vector& input, const std::vector& data); + + int updateTile(TileId tileId, TileVersion version, const std::vector& data); + + ShapeId getMaxShapeId(); + + std::optional findShapeId(const std::string& name, ShapeType type, const Sqlite3::ConstBlob& hash); + + int insertShape(ShapeId shapeId, const std::string& name, ShapeType type, const Sqlite3::ConstBlob& hash); + + private: + Sqlite3::Db mDb; + Sqlite3::Statement mGetMaxTileId; + Sqlite3::Statement mFindTile; + Sqlite3::Statement mGetTileData; + Sqlite3::Statement mInsertTile; + Sqlite3::Statement mUpdateTile; + Sqlite3::Statement mGetMaxShapeId; + Sqlite3::Statement mFindShapeId; + Sqlite3::Statement mInsertShape; + }; +} + +#endif diff --git a/components/detournavigator/navmeshdbutils.cpp b/components/detournavigator/navmeshdbutils.cpp new file mode 100644 index 0000000000..dce6ef3a72 --- /dev/null +++ b/components/detournavigator/navmeshdbutils.cpp @@ -0,0 +1,40 @@ +#include "navmeshdbutils.hpp" +#include "navmeshdb.hpp" +#include "recastmesh.hpp" + +#include + +#include + +namespace DetourNavigator +{ + namespace + { + ShapeId getShapeId(NavMeshDb& db, const std::string& name, ShapeType type, const std::string& hash, ShapeId& nextShapeId) + { + const Sqlite3::ConstBlob hashData {hash.data(), static_cast(hash.size())}; + if (const auto existingShapeId = db.findShapeId(name, type, hashData)) + return *existingShapeId; + const ShapeId newShapeId = nextShapeId; + db.insertShape(newShapeId, name, type, hashData); + Log(Debug::Verbose) << "Added " << name << " " << type << " shape to navmeshdb with id " << newShapeId; + ++nextShapeId.t; + return newShapeId; + } + } + + ShapeId resolveMeshSource(NavMeshDb& db, const MeshSource& source, ShapeId& nextShapeId) + { + switch (source.mAreaType) + { + case AreaType_null: + return getShapeId(db, source.mShape->mFileName, ShapeType::Avoid, source.mShape->mFileHash, nextShapeId); + case AreaType_ground: + return getShapeId(db, source.mShape->mFileName, ShapeType::Collision, source.mShape->mFileHash, nextShapeId); + default: + Log(Debug::Warning) << "Trying to resolve recast mesh source with unsupported area type: " << source.mAreaType; + assert(false); + return ShapeId(0); + } + } +} diff --git a/components/detournavigator/navmeshdbutils.hpp b/components/detournavigator/navmeshdbutils.hpp new file mode 100644 index 0000000000..02b3bdbb00 --- /dev/null +++ b/components/detournavigator/navmeshdbutils.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDBUTILS_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDBUTILS_H + +#include "navmeshdb.hpp" + +namespace DetourNavigator +{ + struct MeshSource; + + ShapeId resolveMeshSource(NavMeshDb& db, const MeshSource& source, ShapeId& nextShapeId); +} + +#endif diff --git a/components/detournavigator/objectid.hpp b/components/detournavigator/objectid.hpp index 9c4b5b2710..22fc792c6f 100644 --- a/components/detournavigator/objectid.hpp +++ b/components/detournavigator/objectid.hpp @@ -15,6 +15,11 @@ namespace DetourNavigator { } + explicit ObjectId(std::size_t value) noexcept + : mValue(value) + { + } + std::size_t value() const noexcept { return mValue; diff --git a/components/detournavigator/offmeshconnectionsmanager.cpp b/components/detournavigator/offmeshconnectionsmanager.cpp index 2306e50da9..a11da21218 100644 --- a/components/detournavigator/offmeshconnectionsmanager.cpp +++ b/components/detournavigator/offmeshconnectionsmanager.cpp @@ -65,11 +65,11 @@ namespace DetourNavigator return removed; } - std::vector OffMeshConnectionsManager::get(const TilePosition& tilePosition) + std::vector OffMeshConnectionsManager::get(const TilePosition& tilePosition) const { std::vector result; - const auto values = mValues.lock(); + const auto values = mValues.lockConst(); const auto itByTilePosition = values->mByTilePosition.find(tilePosition); diff --git a/components/detournavigator/offmeshconnectionsmanager.hpp b/components/detournavigator/offmeshconnectionsmanager.hpp index a0a1231bc4..455b03276a 100644 --- a/components/detournavigator/offmeshconnectionsmanager.hpp +++ b/components/detournavigator/offmeshconnectionsmanager.hpp @@ -24,7 +24,7 @@ namespace DetourNavigator std::set remove(const ObjectId id); - std::vector get(const TilePosition& tilePosition); + std::vector get(const TilePosition& tilePosition) const; private: struct Values diff --git a/components/detournavigator/recastmeshprovider.hpp b/components/detournavigator/recastmeshprovider.hpp new file mode 100644 index 0000000000..5c56d15a5e --- /dev/null +++ b/components/detournavigator/recastmeshprovider.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHPROVIDER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHPROVIDER_H + +#include "tileposition.hpp" +#include "recastmesh.hpp" +#include "tilecachedrecastmeshmanager.hpp" +#include "version.hpp" + +#include +#include + +namespace DetourNavigator +{ + class RecastMesh; + + class RecastMeshProvider + { + public: + RecastMeshProvider(TileCachedRecastMeshManager& impl) + : mImpl(impl) + {} + + std::shared_ptr getMesh(const TilePosition& tilePosition) const + { + return mImpl.get().getNewMesh(tilePosition); + } + + private: + std::reference_wrapper mImpl; + }; +} + +#endif diff --git a/components/detournavigator/serialization.cpp b/components/detournavigator/serialization.cpp new file mode 100644 index 0000000000..8eb60ddb97 --- /dev/null +++ b/components/detournavigator/serialization.cpp @@ -0,0 +1,214 @@ +#include "serialization.hpp" + +#include "dbrefgeometryobject.hpp" +#include "preparednavmeshdata.hpp" +#include "recastmesh.hpp" +#include "settings.hpp" + +#include +#include +#include + +#include +#include + +namespace DetourNavigator +{ +namespace +{ + template + struct Format : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + void operator()(Visitor&& visitor, const osg::Vec2i& value) const + { + visitor(*this, value.ptr(), 2); + } + + template + void operator()(Visitor&& visitor, const osg::Vec2f& value) const + { + visitor(*this, value.ptr(), 2); + } + + template + void operator()(Visitor&& visitor, const osg::Vec3f& value) const + { + visitor(*this, value.ptr(), 3); + } + + template + void operator()(Visitor&& visitor, const Water& value) const + { + visitor(*this, value.mCellSize); + visitor(*this, value.mLevel); + } + + template + void operator()(Visitor&& visitor, const CellWater& value) const + { + visitor(*this, value.mCellPosition); + visitor(*this, value.mWater); + } + + template + void operator()(Visitor&& visitor, const RecastSettings& value) const + { + visitor(*this, value.mCellHeight); + visitor(*this, value.mCellSize); + visitor(*this, value.mDetailSampleDist); + visitor(*this, value.mDetailSampleMaxError); + visitor(*this, value.mMaxClimb); + visitor(*this, value.mMaxSimplificationError); + visitor(*this, value.mMaxSlope); + visitor(*this, value.mRecastScaleFactor); + visitor(*this, value.mSwimHeightScale); + visitor(*this, value.mBorderSize); + visitor(*this, value.mMaxEdgeLen); + visitor(*this, value.mMaxVertsPerPoly); + visitor(*this, value.mRegionMergeArea); + visitor(*this, value.mRegionMinArea); + visitor(*this, value.mTileSize); + } + + template + void operator()(Visitor&& visitor, const TileBounds& value) const + { + visitor(*this, value.mMin); + visitor(*this, value.mMax); + } + + template + void operator()(Visitor&& visitor, const Heightfield& value) const + { + visitor(*this, value.mCellPosition); + visitor(*this, value.mCellSize); + visitor(*this, value.mLength); + visitor(*this, value.mMinHeight); + visitor(*this, value.mMaxHeight); + visitor(*this, value.mHeights); + visitor(*this, value.mOriginalSize); + visitor(*this, value.mMinX); + visitor(*this, value.mMinY); + } + + template + void operator()(Visitor&& visitor, const FlatHeightfield& value) const + { + visitor(*this, value.mCellPosition); + visitor(*this, value.mCellSize); + visitor(*this, value.mHeight); + } + + template + void operator()(Visitor&& visitor, const RecastMesh& value) const + { + visitor(*this, value.getWater()); + visitor(*this, value.getHeightfields()); + visitor(*this, value.getFlatHeightfields()); + } + + template + void operator()(Visitor&& visitor, const ESM::Position& value) const + { + visitor(*this, value.pos); + visitor(*this, value.rot); + } + + template + void operator()(Visitor&& visitor, const ObjectTransform& value) const + { + visitor(*this, value.mPosition); + visitor(*this, value.mScale); + } + + template + void operator()(Visitor&& visitor, const DbRefGeometryObject& value) const + { + visitor(*this, value.mShapeId); + visitor(*this, value.mObjectTransform); + } + + template + void operator()(Visitor&& visitor, const RecastSettings& settings, const RecastMesh& recastMesh, + const std::vector& dbRefGeometryObjects) const + { + visitor(*this, DetourNavigator::recastMeshMagic); + visitor(*this, DetourNavigator::recastMeshVersion); + visitor(*this, settings); + visitor(*this, recastMesh); + visitor(*this, dbRefGeometryObjects); + } + + template + auto operator()(Visitor&& visitor, const rcPolyMesh& value) const + { + visitor(*this, value.nverts); + visitor(*this, value.npolys); + visitor(*this, value.maxpolys); + visitor(*this, value.nvp); + visitor(*this, value.bmin); + visitor(*this, value.bmax); + visitor(*this, value.cs); + visitor(*this, value.ch); + visitor(*this, value.borderSize); + visitor(*this, value.maxEdgeError); + visitor(*this, value.verts, getVertsLength(value)); + visitor(*this, value.polys, getPolysLength(value)); + visitor(*this, value.regs, getRegsLength(value)); + visitor(*this, value.flags, getFlagsLength(value)); + visitor(*this, value.areas, getAreasLength(value)); + } + + template + auto operator()(Visitor&& visitor, const rcPolyMeshDetail& value) const + { + visitor(*this, value.nmeshes); + visitor(*this, value.meshes, getMeshesLength(value)); + visitor(*this, value.nverts); + visitor(*this, value.verts, getVertsLength(value)); + visitor(*this, value.ntris); + visitor(*this, value.tris, getTrisLength(value)); + } + + template + auto operator()(Visitor&& visitor, const PreparedNavMeshData& value) const + { + visitor(*this, DetourNavigator::preparedNavMeshDataMagic); + visitor(*this, DetourNavigator::preparedNavMeshDataVersion); + visitor(*this, value.mUserId); + visitor(*this, value.mCellSize); + visitor(*this, value.mCellHeight); + visitor(*this, value.mPolyMesh); + visitor(*this, value.mPolyMeshDetail); + } + }; +} +} // namespace DetourNavigator + +namespace DetourNavigator +{ + std::vector serialize(const RecastSettings& settings, const RecastMesh& recastMesh, + const std::vector& dbRefGeometryObjects) + { + constexpr Format format; + Serialization::SizeAccumulator sizeAccumulator; + format(sizeAccumulator, settings, recastMesh, dbRefGeometryObjects); + std::vector result(sizeAccumulator.value()); + format(Serialization::BinaryWriter(result.data(), result.data() + result.size()), + settings, recastMesh, dbRefGeometryObjects); + return result; + } + + std::vector serialize(const PreparedNavMeshData& value) + { + constexpr Format format; + Serialization::SizeAccumulator sizeAccumulator; + format(sizeAccumulator, value); + std::vector result(sizeAccumulator.value()); + format(Serialization::BinaryWriter(result.data(), result.data() + result.size()), value); + return result; + } +} diff --git a/components/detournavigator/serialization.hpp b/components/detournavigator/serialization.hpp new file mode 100644 index 0000000000..8e8096a3dc --- /dev/null +++ b/components/detournavigator/serialization.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H + +#include +#include +#include + +namespace DetourNavigator +{ + class RecastMesh; + struct DbRefGeometryObject; + struct PreparedNavMeshData; + struct RecastSettings; + + constexpr char recastMeshMagic[] = {'r', 'c', 's', 't'}; + constexpr std::uint32_t recastMeshVersion = 1; + + constexpr char preparedNavMeshDataMagic[] = {'p', 'n', 'a', 'v'}; + constexpr std::uint32_t preparedNavMeshDataVersion = 1; + + std::vector serialize(const RecastSettings& settings, const RecastMesh& value, + const std::vector& dbRefGeometryObjects); + + std::vector serialize(const PreparedNavMeshData& value); +} + +#endif diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp index 5ad45d699b..cda9ab8d2f 100644 --- a/components/detournavigator/settings.cpp +++ b/components/detournavigator/settings.cpp @@ -61,6 +61,7 @@ namespace DetourNavigator result.mEnableRecastMeshFileNameRevision = ::Settings::Manager::getBool("enable recast mesh file name revision", "Navigator"); result.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator"); result.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator")); + result.mNavMeshVersion = ::Settings::Manager::getInt("nav mesh version", "Navigator"); return result; } diff --git a/components/detournavigator/settings.hpp b/components/detournavigator/settings.hpp index fbd44dfcfc..3253b60893 100644 --- a/components/detournavigator/settings.hpp +++ b/components/detournavigator/settings.hpp @@ -48,6 +48,7 @@ namespace DetourNavigator std::string mRecastMeshPathPrefix; std::string mNavMeshPathPrefix; std::chrono::milliseconds mMinUpdateInterval; + std::int64_t mNavMeshVersion = 0; }; RecastSettings makeRecastSettingsFromSettingsManager(); diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp index ebd64180aa..88fd8ab3f5 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp @@ -196,6 +196,13 @@ namespace DetourNavigator return nullptr; } + std::shared_ptr TileCachedRecastMeshManager::getNewMesh(const TilePosition& tilePosition) const + { + if (const auto manager = getManager(tilePosition)) + return manager->getNewMesh(); + return nullptr; + } + std::size_t TileCachedRecastMeshManager::getRevision() const { return mRevision; diff --git a/components/detournavigator/tilecachedrecastmeshmanager.hpp b/components/detournavigator/tilecachedrecastmeshmanager.hpp index 96183a51be..68dc565fa5 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.hpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.hpp @@ -88,6 +88,8 @@ namespace DetourNavigator std::shared_ptr getCachedMesh(const TilePosition& tilePosition) const; + std::shared_ptr getNewMesh(const TilePosition& tilePosition) const; + template void forEachTile(Function&& function) const { diff --git a/components/sqlite3/request.hpp b/components/sqlite3/request.hpp index 339c7f7521..0a74bf1cb3 100644 --- a/components/sqlite3/request.hpp +++ b/components/sqlite3/request.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_SQLITE3_REQUEST_H #include "statement.hpp" +#include "types.hpp" #include @@ -53,6 +54,13 @@ namespace Sqlite3 + ": " + std::string(sqlite3_errmsg(&db))); } + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, const ConstBlob& value) + { + if (sqlite3_bind_blob(&stmt, index, value.mData, value.mSize, SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind blob to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + template inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, const char* name, const T& value) { diff --git a/components/sqlite3/types.hpp b/components/sqlite3/types.hpp new file mode 100644 index 0000000000..325e9e6608 --- /dev/null +++ b/components/sqlite3/types.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_TYPES_H +#define OPENMW_COMPONENTS_SQLITE3_TYPES_H + +#include + +namespace Sqlite3 +{ + struct ConstBlob + { + const char* mData; + int mSize; + }; +} + +#endif diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 605be3f6af..6b19885557 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -464,3 +464,4 @@ default actor pathfind half extents :Default: 29.27999496459961 28.479997634887695 66.5 Actor half extents used for exterior cells to generate navmesh. +Changing the value will invalidate navmesh disk cache. diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index f8ab4522d6..9132364d48 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -206,6 +206,17 @@ Absent pieces usually mean a bug in recast mesh tiles building. Allows to do in-game debug. Potentially decreases performance. +nav mesh version +---------------- + +:Type: integer +:Range: > 0 +:Default: 1 + +Version of navigation mesh generation algorithm. +Should be increased each time there is a difference between output of makeNavMeshTileData function for the same input. +Changing the value will invalidate navmesh disk cache. + Expert settings *************** diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 084aad4fe3..e80d6d1106 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -930,6 +930,10 @@ min update interval ms = 250 # Distance is measured in the number of tiles and can be only an integer value. wait until min distance to player = 5 +# Version of navigation mesh generation algorithm. +# Should be increased each time there is a difference between output of makeNavMeshTileData function for the same input. +nav mesh version = 1 + [Shadows] # Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true.