Add a binary to generate navmesh from content files

Load content files based on the engine config files. Generate navmesh per cell
for all cells and store into SQLite database.
pull/3225/head
elsid 3 years ago
parent b5c689976e
commit 953a4c5550
No known key found for this signature in database
GPG Key ID: B845CB9FEE18AB40

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

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

@ -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}" \

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

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

@ -0,0 +1,209 @@
#include "worldspacedata.hpp"
#include "navmesh.hpp"
#include <components/debug/debugging.hpp>
#include <components/detournavigator/navmeshdb.hpp>
#include <components/detournavigator/recastglobalallocator.hpp>
#include <components/esm/esmreader.hpp>
#include <components/esm/variant.hpp>
#include <components/esmloader/esmdata.hpp>
#include <components/esmloader/load.hpp>
#include <components/fallback/fallback.hpp>
#include <components/fallback/validate.hpp>
#include <components/files/configurationmanager.hpp>
#include <components/resource/bulletshapemanager.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/resource/niffilemanager.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/settings/settings.hpp>
#include <components/to_utf8/to_utf8.hpp>
#include <components/version/version.hpp>
#include <components/vfs/manager.hpp>
#include <components/vfs/registerarchives.hpp>
#include <osg/Vec3f>
#include <boost/filesystem.hpp>
#include <boost/program_options.hpp>
#include <cstddef>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace NavMeshTool
{
namespace
{
namespace bpo = boost::program_options;
using StringsVector = std::vector<std::string>;
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<Files::MaybeQuotedPathContainer>()->default_value(Files::MaybeQuotedPathContainer(), "data")
->multitoken()->composing(), "set data directories (later directories have higher priority)")
("data-local", bpo::value<Files::MaybeQuotedPathContainer::value_type>()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""),
"set local data directory (highest priority)")
("fallback-archive", bpo::value<StringsVector>()->default_value(StringsVector(), "fallback-archive")
->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)")
("resources", bpo::value<Files::MaybeQuotedPath>()->default_value(Files::MaybeQuotedPath(), "resources"),
"set resources directory")
("content", bpo::value<StringsVector>()->default_value(StringsVector(), "")
->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts")
("fs-strict", bpo::value<bool>()->implicit_value(true)
->default_value(false), "strict file system handling (no case folding)")
("encoding", bpo::value<std::string>()->
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<Fallback::FallbackMap>()->default_value(Fallback::FallbackMap(), "")
->multitoken()->composing(), "fallback values")
("threads", bpo::value<std::size_t>()->default_value(std::max<std::size_t>(std::thread::hardware_concurrency() - 1, 1)),
"number of threads for parallel processing")
("process-interior-cells", bpo::value<bool>()->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<std::string>());
Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding);
ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding));
Files::PathContainer dataDirs(asPathContainer(variables["data"].as<Files::MaybeQuotedPathContainer>()));
auto local = variables["data-local"].as<Files::MaybeQuotedPathContainer::value_type>();
if (!local.empty())
dataDirs.push_back(std::move(local));
config.processPaths(dataDirs);
const auto fsStrict = variables["fs-strict"].as<bool>();
const auto resDir = variables["resources"].as<Files::MaybeQuotedPath>();
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<StringsVector>();
const auto contentFiles = variables["content"].as<StringsVector>();
const std::size_t threadsNumber = variables["threads"].as<std::size_t>();
if (threadsNumber < 1)
{
std::cerr << "Invalid threads number: " << threadsNumber << ", expected >= 1";
return -1;
}
const bool processInteriorCells = variables["process-interior-cells"].as<bool>();
Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().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<ESM::ESMReader> 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");
}

@ -0,0 +1,212 @@
#include "navmesh.hpp"
#include "worldspacedata.hpp"
#include <components/bullethelpers/aabb.hpp>
#include <components/debug/debuglog.hpp>
#include <components/detournavigator/generatenavmeshtile.hpp>
#include <components/detournavigator/gettilespositions.hpp>
#include <components/detournavigator/navmeshdb.hpp>
#include <components/detournavigator/navmeshdbutils.hpp>
#include <components/detournavigator/offmeshconnection.hpp>
#include <components/detournavigator/offmeshconnectionsmanager.hpp>
#include <components/detournavigator/preparednavmeshdata.hpp>
#include <components/detournavigator/recastmesh.hpp>
#include <components/detournavigator/recastmeshprovider.hpp>
#include <components/detournavigator/serialization.hpp>
#include <components/detournavigator/tilecachedrecastmeshmanager.hpp>
#include <components/detournavigator/tileposition.hpp>
#include <components/esm/loadcell.hpp>
#include <components/misc/guarded.hpp>
#include <components/misc/progressreporter.hpp>
#include <components/sceneutil/workqueue.hpp>
#include <components/sqlite3/transaction.hpp>
#include <DetourNavMesh.h>
#include <osg/Vec3f>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
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<double>(provided) / static_cast<double>(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<NavMeshTileInfo> find(const std::string& worldspace, const TilePosition &tilePosition,
const std::vector<std::byte> &input) override
{
std::optional<NavMeshTileInfo> 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<std::byte>& input, PreparedNavMeshData& data) override
{
data.mUserId = static_cast<unsigned>(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<unsigned>(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<LogGeneratedTiles> 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<NavMeshTileConsumer>(std::move(db));
std::size_t tiles = 0;
for (const std::unique_ptr<WorldspaceNavMeshInput>& 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";
}
}

@ -0,0 +1,23 @@
#ifndef OPENMW_NAVMESHTOOL_NAVMESH_H
#define OPENMW_NAVMESHTOOL_NAVMESH_H
#include <osg/Vec3f>
#include <cstddef>
#include <string_view>
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

@ -0,0 +1,329 @@
#include "worldspacedata.hpp"
#include <components/bullethelpers/aabb.hpp>
#include <components/bullethelpers/heightfield.hpp>
#include <components/debug/debuglog.hpp>
#include <components/detournavigator/gettilespositions.hpp>
#include <components/detournavigator/objectid.hpp>
#include <components/detournavigator/recastmesh.hpp>
#include <components/detournavigator/tilecachedrecastmeshmanager.hpp>
#include <components/esm/cellref.hpp>
#include <components/esm/esmreader.hpp>
#include <components/esm/loadcell.hpp>
#include <components/esm/loadland.hpp>
#include <components/esmloader/esmdata.hpp>
#include <components/esmloader/lessbyid.hpp>
#include <components/esmloader/record.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/misc/stringops.hpp>
#include <components/resource/bulletshapemanager.hpp>
#include <components/settings/settings.hpp>
#include <components/vfs/manager.hpp>
#include <LinearMath/btVector3.h>
#include <osg/Vec2i>
#include <osg/Vec3f>
#include <osg/ref_ptr>
#include <algorithm>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
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<CellRef> loadCellRefs(const ESM::Cell& cell, const EsmLoader::EsmData& esmData,
std::vector<ESM::ESMReader>& readers)
{
std::vector<EsmLoader::Record<CellRef>> cellRefs;
for (std::size_t i = 0; i < cell.mContextList.size(); i++)
{
ESM::ESMReader& reader = readers[static_cast<std::size_t>(cell.mContextList[i].index)];
cell.restore(reader, static_cast<int>(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<CellRef>& v) -> const ESM::RefNum& { return v.mValue.mRefNum; };
std::vector<CellRef> result = prepareRecords(cellRefs, getKey);
Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs";
return result;
}
template <class F>
void forEachObject(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, const VFS::Manager& vfs,
Resource::BulletShapeManager& bulletShapeManager, std::vector<ESM::ESMReader>& readers,
F&& f)
{
std::vector<CellRef> 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<const Resource::BulletShape> 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<const Resource::BulletShape>();
}
} ();
if (shape == nullptr || shape->mCollisionShape == nullptr)
continue;
osg::ref_ptr<Resource::BulletShapeInstance> 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<btScalar>(cellPosition.x() * ESM::Land::REAL_SIZE),
static_cast<btScalar>(cellPosition.y() * ESM::Land::REAL_SIZE),
minHeight
);
aabb.m_min = btVector3(
static_cast<btScalar>((cellPosition.x() + 1) * ESM::Land::REAL_SIZE),
static_cast<btScalar>((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<HeightfieldShape, float, float> makeHeightfieldShape(const std::optional<ESM::Land>& land,
const osg::Vec2i& cellPosition, std::vector<std::vector<float>>& heightfields,
std::vector<std::unique_ptr<ESM::Land::LandData>>& 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<ESM::Land::LandData>());
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<std::size_t>(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<ESM::ESMReader>& readers,
const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData,
bool processInteriorCells)
{
Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells...";
std::map<std::string_view, std::unique_ptr<WorldspaceNavMeshInput>> 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<WorldspaceNavMeshInput>(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<ESM::Land>() : *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<int>::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;
}
}

@ -0,0 +1,97 @@
#ifndef OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H
#define OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H
#include <components/bullethelpers/collisionobject.hpp>
#include <components/detournavigator/tilecachedrecastmeshmanager.hpp>
#include <components/esm/loadland.hpp>
#include <components/misc/convert.hpp>
#include <components/resource/bulletshape.hpp>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <BulletCollision/Gimpact/btBoxCollision.h>
#include <LinearMath/btVector3.h>
#include <memory>
#include <string>
#include <vector>
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<Resource::BulletShapeInstance>&& 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<Resource::BulletShapeInstance>& getShapeInstance() const noexcept { return mShapeInstance; }
const DetourNavigator::ObjectTransform& getObjectTransform() const noexcept { return mObjectTransform; }
btCollisionObject& getCollisionObject() const noexcept { return *mCollisionObject; }
private:
osg::ref_ptr<Resource::BulletShapeInstance> mShapeInstance;
DetourNavigator::ObjectTransform mObjectTransform;
std::unique_ptr<btCollisionObject> mCollisionObject;
};
struct WorldspaceData
{
std::vector<std::unique_ptr<WorldspaceNavMeshInput>> mNavMeshInputs;
std::vector<BulletObject> mObjects;
std::vector<std::unique_ptr<ESM::Land::LandData>> mLandData;
std::vector<std::vector<float>> mHeightfields;
};
WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, std::vector<ESM::ESMReader>& readers,
const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData,
bool processInteriorCells);
}
#endif

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

@ -0,0 +1,51 @@
#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H
#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H
#include <algorithm>
#include <numeric>
#include <random>
#include <type_traits>
namespace DetourNavigator
{
namespace Tests
{
template <class T, class Random>
inline auto generateValue(T& value, Random& random)
-> std::enable_if_t<sizeof(T) >= 2>
{
using Distribution = std::conditional_t<
std::is_floating_point_v<T>,
std::uniform_real_distribution<T>,
std::uniform_int_distribution<T>
>;
Distribution distribution(std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
value = distribution(random);
}
template <class T, class Random>
inline auto generateValue(T& value, Random& random)
-> std::enable_if_t<sizeof(T) == 1>
{
unsigned short v;
generateValue(v, random);
value = static_cast<T>(v % 256);
}
template <class Random>
inline void generateValue(unsigned char& value, Random& random)
{
unsigned short v;
generateValue(v, random);
value = static_cast<unsigned char>(v % 256);
}
template <class I, class Random>
inline void generateRange(I begin, I end, Random& random)
{
std::for_each(begin, end, [&] (auto& v) { generateValue(v, random); });
}
}
}
#endif

@ -0,0 +1,112 @@
#include "generate.hpp"
#include <components/detournavigator/navmeshdb.hpp>
#include <components/esm/cellid.hpp>
#include <DetourAlloc.h>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <numeric>
#include <random>
namespace
{
using namespace testing;
using namespace DetourNavigator;
using namespace DetourNavigator::Tests;
struct Tile
{
std::string mWorldspace;
TilePosition mTilePosition;
std::vector<std::byte> mInput;
std::vector<std::byte> mData;
};
struct DetourNavigatorNavMeshDbTest : Test
{
NavMeshDb mDb {":memory:"};
std::minstd_rand mRandom;
std::vector<std::byte> generateData()
{
std::vector<std::byte> 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<std::byte> input = generateData();
std::vector<std::byte> 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<std::byte> input = generateData();
const std::vector<std::byte> 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<std::byte> input = generateData();
const std::vector<std::byte> 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));
}
}

@ -1,4 +1,5 @@
#include "operators.hpp"
#include "generate.hpp"
#include <components/detournavigator/navmeshtilescache.hpp>
#include <components/detournavigator/exceptions.hpp>
@ -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 <class T>
void generate(T*& values, int size)
template <class T, class Random>
void generateRecastArray(T*& values, int size, Random& random)
{
values = static_cast<T*>(permRecastAlloc(size * sizeof(T)));
std::generate_n(values, static_cast<std::size_t>(size), [] { return static_cast<T>(std::rand()); });
generateRange(values, values + static_cast<std::ptrdiff_t>(size), random);
}
void generate(rcPolyMesh& value, int size)
template <class Random>
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 <class Random>
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 <class Random>
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<PreparedNavMeshData> makePeparedNavMeshData(int size)
{
std::minstd_rand random;
auto result = std::make_unique<PreparedNavMeshData>();
generate(*result, size);
generate(*result, size, random);
return result;
}

@ -204,6 +204,9 @@ add_component_dir(detournavigator
navmeshcacheitem
navigatorutils
generatenavmeshtile
navmeshdb
serialization
navmeshdbutils
)
add_component_dir(loadinglistener

@ -84,6 +84,11 @@ namespace DetourNavigator
return *mCached.lockConst();
}
std::shared_ptr<RecastMesh> CachedRecastMeshManager::getNewMesh() const
{
return mImpl.getMesh();
}
bool CachedRecastMeshManager::isEmpty() const
{
return mImpl.isEmpty();

@ -35,6 +35,8 @@ namespace DetourNavigator
std::shared_ptr<RecastMesh> getCachedMesh() const;
std::shared_ptr<RecastMesh> getNewMesh() const;
bool isEmpty() const;
void reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion);

@ -0,0 +1,46 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DBREFGEOMETRYOBJECT_H
#include "objecttransform.hpp"
#include "recastmesh.hpp"
#include <algorithm>
#include <cstdint>
#include <tuple>
#include <vector>
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 <class ResolveMeshSource>
inline std::vector<DbRefGeometryObject> makeDbRefGeometryObjects(const std::vector<MeshSource>& meshSources,
ResolveMeshSource&& resolveMeshSource)
{
std::vector<DbRefGeometryObject> 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

@ -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 <components/debug/debuglog.hpp>
#include <osg/Vec3f>
#include <osg/io_utils>
#include <memory>
#include <stdexcept>
#include <vector>
#include <optional>
#include <functional>
namespace DetourNavigator
{
namespace
{
struct Ignore
{
std::shared_ptr<NavMeshTileConsumer> 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<NavMeshTileConsumer> 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> recastMesh = mRecastMeshProvider.getMesh(mTilePosition);
if (recastMesh == nullptr || isEmpty(*recastMesh))
return;
const std::vector<DbRefGeometryObject> objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(),
[&] (const MeshSource& v) { return consumer->resolveMeshSource(v); });
std::vector<std::byte> input = serialize(mSettings.mRecast, *recastMesh, objects);
const std::optional<NavMeshTileInfo> 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();
}
}
}

@ -0,0 +1,71 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_GENERATENAVMESHTILE_H
#include "recastmeshprovider.hpp"
#include "tileposition.hpp"
#include <components/sceneutil/workqueue.hpp>
#include <osg/Vec3f>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string_view>
#include <vector>
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<NavMeshTileInfo> find(const std::string& worldspace, const TilePosition& tilePosition,
const std::vector<std::byte>& input) = 0;
virtual void ignore() = 0;
virtual void insert(const std::string& worldspace, const TilePosition& tilePosition,
std::int64_t version, const std::vector<std::byte>& 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<NavMeshTileConsumer> 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<NavMeshTileConsumer> mConsumer;
inline void impl() noexcept;
};
}
#endif

@ -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<int>(std::ceil(getHeight(settings, agentHalfExtents) / settings.mCellHeight));
result.mWalkableClimb = static_cast<int>(std::floor(getMaxClimb(settings) / settings.mCellHeight));
result.mWalkableRadius = static_cast<int>(std::ceil(getRadius(settings, agentHalfExtents) / settings.mCellSize));
result.mMaxEdgeLen = static_cast<int>(std::round(static_cast<float>(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<int>(std::ceil(getHeight(settings, agentHalfExtents) / settings.mCellHeight));
result.mWalkableClimb = static_cast<int>(std::floor(getMaxClimb(settings) / settings.mCellHeight));
result.mWalkableRadius = static_cast<int>(std::ceil(getRadius(settings, agentHalfExtents) / settings.mCellSize));
result.mMaxEdgeLen = static_cast<int>(std::round(static_cast<float>(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<PreparedNavMeshData> 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);

@ -14,6 +14,7 @@
#include <vector>
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<PreparedNavMeshData> prepareNavMeshTileData(const RecastMesh& recastMesh, const TilePosition& tile,
const Bounds& bounds, const osg::Vec3f& agentHalfExtents, const Settings& settings);
std::unique_ptr<PreparedNavMeshData> prepareNavMeshTileData(const RecastMesh& recastMesh,
const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents, const RecastSettings& settings);
NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data,
const std::vector<OffMeshConnection>& offMeshConnections, const osg::Vec3f& agentHalfExtents,

@ -0,0 +1,296 @@
#include "navmeshdb.hpp"
#include <components/debug/debuglog.hpp>
#include <components/misc/compression.hpp>
#include <components/sqlite3/db.hpp>
#include <components/sqlite3/request.hpp>
#include <DetourAlloc.h>
#include <sqlite3.h>
#include <cstddef>
#include <string>
#include <string_view>
#include <vector>
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<std::underlying_type_t<ShapeType>>(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<Tile> NavMeshDb::findTile(const std::string& worldspace,
const TilePosition& tilePosition, const std::vector<std::byte>& input)
{
Tile result;
auto row = std::tie(result.mTileId, result.mVersion);
const std::vector<std::byte> compressedInput = Misc::compress(input);
if (&row == request(*mDb, mFindTile, &row, 1, worldspace, tilePosition, compressedInput))
return {};
return result;
}
std::optional<TileData> NavMeshDb::getTileData(const std::string& worldspace,
const TilePosition& tilePosition, const std::vector<std::byte>& input)
{
TileData result;
auto row = std::tie(result.mTileId, result.mVersion, result.mData);
const std::vector<std::byte> 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<std::byte>& input, const std::vector<std::byte>& data)
{
const std::vector<std::byte> compressedInput = Misc::compress(input);
const std::vector<std::byte> compressedData = Misc::compress(data);
return execute(*mDb, mInsertTile, tileId, worldspace, tilePosition, version, compressedInput, compressedData);
}
int NavMeshDb::updateTile(TileId tileId, TileVersion version, const std::vector<std::byte>& data)
{
const std::vector<std::byte> 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<ShapeId> 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<std::byte>& 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<std::byte>& 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<std::byte>& input,
const std::vector<std::byte>& 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<std::byte>& 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<int>(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<int>(type));
Sqlite3::bindParameter(db, statement, ":hash", hash);
}
}
}

@ -0,0 +1,153 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDB_H
#include "tileposition.hpp"
#include <components/sqlite3/db.hpp>
#include <components/sqlite3/statement.hpp>
#include <components/sqlite3/transaction.hpp>
#include <components/sqlite3/types.hpp>
#include <boost/serialization/strong_typedef.hpp>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include <memory>
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<std::byte> 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<std::byte>& 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<std::byte>& 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<std::byte>& input,
const std::vector<std::byte>& data);
};
struct UpdateTile
{
static std::string_view text() noexcept;
static void bind(sqlite3& db, sqlite3_stmt& statement, TileId tileId, TileVersion version,
const std::vector<std::byte>& 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<Tile> findTile(const std::string& worldspace,
const TilePosition& tilePosition, const std::vector<std::byte>& input);
std::optional<TileData> getTileData(const std::string& worldspace,
const TilePosition& tilePosition, const std::vector<std::byte>& input);
int insertTile(TileId tileId, const std::string& worldspace, const TilePosition& tilePosition,
TileVersion version, const std::vector<std::byte>& input, const std::vector<std::byte>& data);
int updateTile(TileId tileId, TileVersion version, const std::vector<std::byte>& data);
ShapeId getMaxShapeId();
std::optional<ShapeId> 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<DbQueries::GetMaxTileId> mGetMaxTileId;
Sqlite3::Statement<DbQueries::FindTile> mFindTile;
Sqlite3::Statement<DbQueries::GetTileData> mGetTileData;
Sqlite3::Statement<DbQueries::InsertTile> mInsertTile;
Sqlite3::Statement<DbQueries::UpdateTile> mUpdateTile;
Sqlite3::Statement<DbQueries::GetMaxShapeId> mGetMaxShapeId;
Sqlite3::Statement<DbQueries::FindShapeId> mFindShapeId;
Sqlite3::Statement<DbQueries::InsertShape> mInsertShape;
};
}
#endif

@ -0,0 +1,40 @@
#include "navmeshdbutils.hpp"
#include "navmeshdb.hpp"
#include "recastmesh.hpp"
#include <components/debug/debuglog.hpp>
#include <cassert>
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<int>(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);
}
}
}

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

@ -15,6 +15,11 @@ namespace DetourNavigator
{
}
explicit ObjectId(std::size_t value) noexcept
: mValue(value)
{
}
std::size_t value() const noexcept
{
return mValue;

@ -65,11 +65,11 @@ namespace DetourNavigator
return removed;
}
std::vector<OffMeshConnection> OffMeshConnectionsManager::get(const TilePosition& tilePosition)
std::vector<OffMeshConnection> OffMeshConnectionsManager::get(const TilePosition& tilePosition) const
{
std::vector<OffMeshConnection> result;
const auto values = mValues.lock();
const auto values = mValues.lockConst();
const auto itByTilePosition = values->mByTilePosition.find(tilePosition);

@ -24,7 +24,7 @@ namespace DetourNavigator
std::set<TilePosition> remove(const ObjectId id);
std::vector<OffMeshConnection> get(const TilePosition& tilePosition);
std::vector<OffMeshConnection> get(const TilePosition& tilePosition) const;
private:
struct Values

@ -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 <functional>
#include <memory>
namespace DetourNavigator
{
class RecastMesh;
class RecastMeshProvider
{
public:
RecastMeshProvider(TileCachedRecastMeshManager& impl)
: mImpl(impl)
{}
std::shared_ptr<RecastMesh> getMesh(const TilePosition& tilePosition) const
{
return mImpl.get().getNewMesh(tilePosition);
}
private:
std::reference_wrapper<TileCachedRecastMeshManager> mImpl;
};
}
#endif

@ -0,0 +1,214 @@
#include "serialization.hpp"
#include "dbrefgeometryobject.hpp"
#include "preparednavmeshdata.hpp"
#include "recastmesh.hpp"
#include "settings.hpp"
#include <components/serialization/binarywriter.hpp>
#include <components/serialization/format.hpp>
#include <components/serialization/sizeaccumulator.hpp>
#include <cstddef>
#include <vector>
namespace DetourNavigator
{
namespace
{
template <Serialization::Mode mode>
struct Format : Serialization::Format<mode, Format<mode>>
{
using Serialization::Format<mode, Format<mode>>::operator();
template <class Visitor>
void operator()(Visitor&& visitor, const osg::Vec2i& value) const
{
visitor(*this, value.ptr(), 2);
}
template <class Visitor>
void operator()(Visitor&& visitor, const osg::Vec2f& value) const
{
visitor(*this, value.ptr(), 2);
}
template <class Visitor>
void operator()(Visitor&& visitor, const osg::Vec3f& value) const
{
visitor(*this, value.ptr(), 3);
}
template <class Visitor>
void operator()(Visitor&& visitor, const Water& value) const
{
visitor(*this, value.mCellSize);
visitor(*this, value.mLevel);
}
template <class Visitor>
void operator()(Visitor&& visitor, const CellWater& value) const
{
visitor(*this, value.mCellPosition);
visitor(*this, value.mWater);
}
template <class Visitor>
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 <class Visitor>
void operator()(Visitor&& visitor, const TileBounds& value) const
{
visitor(*this, value.mMin);
visitor(*this, value.mMax);
}
template <class Visitor>
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 <class Visitor>
void operator()(Visitor&& visitor, const FlatHeightfield& value) const
{
visitor(*this, value.mCellPosition);
visitor(*this, value.mCellSize);
visitor(*this, value.mHeight);
}
template <class Visitor>
void operator()(Visitor&& visitor, const RecastMesh& value) const
{
visitor(*this, value.getWater());
visitor(*this, value.getHeightfields());
visitor(*this, value.getFlatHeightfields());
}
template <class Visitor>
void operator()(Visitor&& visitor, const ESM::Position& value) const
{
visitor(*this, value.pos);
visitor(*this, value.rot);
}
template <class Visitor>
void operator()(Visitor&& visitor, const ObjectTransform& value) const
{
visitor(*this, value.mPosition);
visitor(*this, value.mScale);
}
template <class Visitor>
void operator()(Visitor&& visitor, const DbRefGeometryObject& value) const
{
visitor(*this, value.mShapeId);
visitor(*this, value.mObjectTransform);
}
template <class Visitor>
void operator()(Visitor&& visitor, const RecastSettings& settings, const RecastMesh& recastMesh,
const std::vector<DbRefGeometryObject>& dbRefGeometryObjects) const
{
visitor(*this, DetourNavigator::recastMeshMagic);
visitor(*this, DetourNavigator::recastMeshVersion);
visitor(*this, settings);
visitor(*this, recastMesh);
visitor(*this, dbRefGeometryObjects);
}
template <class Visitor>
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 <class Visitor>
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 <class Visitor>
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<std::byte> serialize(const RecastSettings& settings, const RecastMesh& recastMesh,
const std::vector<DbRefGeometryObject>& dbRefGeometryObjects)
{
constexpr Format<Serialization::Mode::Write> format;
Serialization::SizeAccumulator sizeAccumulator;
format(sizeAccumulator, settings, recastMesh, dbRefGeometryObjects);
std::vector<std::byte> result(sizeAccumulator.value());
format(Serialization::BinaryWriter(result.data(), result.data() + result.size()),
settings, recastMesh, dbRefGeometryObjects);
return result;
}
std::vector<std::byte> serialize(const PreparedNavMeshData& value)
{
constexpr Format<Serialization::Mode::Write> format;
Serialization::SizeAccumulator sizeAccumulator;
format(sizeAccumulator, value);
std::vector<std::byte> result(sizeAccumulator.value());
format(Serialization::BinaryWriter(result.data(), result.data() + result.size()), value);
return result;
}
}

@ -0,0 +1,27 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_H
#include <cstddef>
#include <cstdint>
#include <vector>
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<std::byte> serialize(const RecastSettings& settings, const RecastMesh& value,
const std::vector<DbRefGeometryObject>& dbRefGeometryObjects);
std::vector<std::byte> serialize(const PreparedNavMeshData& value);
}
#endif

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

@ -48,6 +48,7 @@ namespace DetourNavigator
std::string mRecastMeshPathPrefix;
std::string mNavMeshPathPrefix;
std::chrono::milliseconds mMinUpdateInterval;
std::int64_t mNavMeshVersion = 0;
};
RecastSettings makeRecastSettingsFromSettingsManager();

@ -196,6 +196,13 @@ namespace DetourNavigator
return nullptr;
}
std::shared_ptr<RecastMesh> 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;

@ -88,6 +88,8 @@ namespace DetourNavigator
std::shared_ptr<RecastMesh> getCachedMesh(const TilePosition& tilePosition) const;
std::shared_ptr<RecastMesh> getNewMesh(const TilePosition& tilePosition) const;
template <class Function>
void forEachTile(Function&& function) const
{

@ -2,6 +2,7 @@
#define OPENMW_COMPONENTS_SQLITE3_REQUEST_H
#include "statement.hpp"
#include "types.hpp"
#include <sqlite3.h>
@ -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 <typename T>
inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, const char* name, const T& value)
{

@ -0,0 +1,15 @@
#ifndef OPENMW_COMPONENTS_SQLITE3_TYPES_H
#define OPENMW_COMPONENTS_SQLITE3_TYPES_H
#include <cstddef>
namespace Sqlite3
{
struct ConstBlob
{
const char* mData;
int mSize;
};
}
#endif

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

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

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

Loading…
Cancel
Save