#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 #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; using DetourNavigator::TilesPositionsRange; struct CellRef { ESM::RecNameInts mType; ESM::RefNum mRefNum; ESM::RefId mRefId; float mScale; ESM::Position mPos; CellRef( ESM::RecNameInts type, ESM::RefNum refNum, ESM::RefId&& refId, float scale, const ESM::Position& pos) : mType(type) , mRefNum(refNum) , mRefId(std::move(refId)) , mScale(scale) , mPos(pos) { } }; struct AddedCellRef { std::string mCell; CellRef mCellRef; TilesPositionsRange mRange; }; struct WriteArray { const float (&mValue)[3]; friend inline std::ostream& operator<<(std::ostream& stream, const WriteArray& value) { for (std::size_t i = 0; i < 2; ++i) stream << value.mValue[i] << ", "; return stream << value.mValue[2]; } }; ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, const ESM::RefId& 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, ESM::ReadersCache& readers) { std::vector> cellRefs; for (std::size_t i = 0; i < cell.mContextList.size(); i++) { ESM::ReadersCache::BusyItem reader = readers.get(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)) { 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) -> 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, ESM::ReadersCache& readers, F&& f) { std::vector cellRefs = loadCellRefs(cell, esmData, readers); Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs"; for (const CellRef& cellRef : cellRefs) { VFS::Path::Normalized 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(Misc::ResourceHelpers::correctMeshPath(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), cellRef); 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_max = 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{ static_cast(ESM::Land::DEFAULT_HEIGHT) }, static_cast(ESM::Land::DEFAULT_HEIGHT), static_cast(ESM::Land::DEFAULT_HEIGHT) }; ESM::Land::LandData& landData = *landDatas.emplace_back(std::make_unique()); land->loadData(ESM::Land::DATA_VHGT, landData); heightfields.push_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 }; } template void serializeToStderr(const T& value) { const std::vector data = serialize(value); Debug::getRawStderr().write( reinterpret_cast(data.data()), static_cast(data.size())); } std::string makeAddObjectErrorMessage( ObjectId objectId, DetourNavigator::AreaType areaType, const CollisionShape& shape) { std::ostringstream stream; stream << "Failed to add object to recast mesh objectId=" << objectId.value() << " areaType=" << areaType << " fileName=" << shape.getInstance()->mFileName << " fileHash=" << Misc::StringUtils::toHex(shape.getInstance()->mFileHash); return stream.str(); } void detectDisconnectedTileGroups(ESM::RefId worldspace, const std::map& changedTiles, std::span cellRefs) { if (changedTiles.empty()) return; std::deque queue; std::map positionToComponent; std::size_t componentIndex = 0; for (const auto& [initial, changeType] : changedTiles) { if (positionToComponent.contains(initial)) continue; queue.push_back(initial); positionToComponent.emplace(initial, componentIndex); while (!queue.empty()) { const osg::Vec2i position = queue.front(); queue.pop_front(); for (int x = position.x() - 1; x <= position.x() + 1; ++x) { for (int y = position.y() - 1; y <= position.y() + 1; ++y) { const osg::Vec2i candidate(x, y); if (candidate == position) continue; if (!changedTiles.contains(candidate)) continue; const auto it = positionToComponent.find(candidate); if (it != positionToComponent.end()) continue; queue.push_back(candidate); positionToComponent.emplace_hint(it, candidate, componentIndex); } } } ++componentIndex; } if (componentIndex <= 1) return; Log(Debug::Warning) << "Found " << componentIndex << " disconnected tile groups"; std::vector cellRefsPerComponent(componentIndex); for (const AddedCellRef& v : cellRefs) ++cellRefsPerComponent[positionToComponent.at(v.mRange.mBegin)]; const std::size_t largestComponent = std::max_element(cellRefsPerComponent.begin(), cellRefsPerComponent.end()) - cellRefsPerComponent.begin(); for (const AddedCellRef& v : cellRefs) { const std::size_t component = positionToComponent.at(v.mRange.mBegin); if (component == largestComponent) continue; Log(Debug::Warning) << "CellRef belongs to not largest disconnected tile group:" << " worldspace=" << worldspace << " cell=\"" << v.mCell << "\"" << " ref_num=" << v.mCellRef.mRefNum << " ref_id=" << v.mCellRef.mRefId << " scale=" << v.mCellRef.mScale << " pos=" << WriteArray{ v.mCellRef.mPos.pos } << " rot=" << WriteArray{ v.mCellRef.mPos.rot } << " begin_tile=" << v.mRange.mBegin.x() << ", " << v.mRange.mBegin.y() << " end_tile=" << v.mRange.mEnd.x() << ", " << v.mRange.mEnd.y() << " component=" << component; } } } WorldspaceData::WorldspaceData(ESM::RefId worldspace, const DetourNavigator::RecastSettings& settings) : mWorldspace(worldspace) , mTileCachedRecastMeshManager(std::make_unique(settings)) { mAabb.m_min = btVector3(0, 0, 0); mAabb.m_max = btVector3(0, 0, 0); } std::unordered_map> collectWorldspaceCells( const EsmLoader::EsmData& esmData, bool processInteriorCells, const std::regex& worldspaceFilter) { Log(Debug::Info) << "Collecting worldspaces from " << esmData.mCells.size() << " cells..."; std::unordered_map> result; 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::Verbose) << "Skipped interior" << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; continue; } const ESM::RefId cellWorldspace = cell.isExterior() ? ESM::Cell::sDefaultWorldspaceId : cell.mId; if (!std::regex_match(cellWorldspace.toString(), worldspaceFilter)) { Log(Debug::Verbose) << "Skipped filtered out" << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\" from " << cellWorldspace << " worldspace"; continue; } result[cellWorldspace].push_back(i); Log(Debug::Info) << "Collected " << (exterior ? "exterior" : "interior") << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") " << cell.getDescription(); } Log(Debug::Info) << "Collected " << result.size() << " worldspaces"; return result; } WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, ESM::ReadersCache& readers, const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, bool writeBinaryLog, ESM::RefId worldspace, std::span cells) { Log(Debug::Info) << "Processing " << cells.size() << " cells from worldspace " << worldspace << "..."; if (writeBinaryLog) serializeToStderr(ExpectedCells{ static_cast(cells.size()) }); WorldspaceData data(worldspace, settings.mRecast); TileCachedRecastMeshManager& manager = *data.mTileCachedRecastMeshManager; const auto guard = manager.makeUpdateGuard(); manager.setWorldspace(worldspace, guard.get()); std::size_t objectsCounter = 0; std::vector addedCellRefs; for (std::size_t i = 0; i < cells.size(); ++i) { const ESM::Cell& cell = esmData.mCells[cells[i]]; const bool exterior = cell.isExterior(); Log(Debug::Debug) << "Processing " << (exterior ? "exterior" : "interior") << " cell (" << (i + 1) << "/" << cells.size() << ") \"" << cell.getDescription() << "\""; const osg::Vec2i cellPosition(cell.mData.mX, cell.mData.mY); const std::size_t cellObjectsBegin = data.mObjects.size(); 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), data.mAabb, data.mAabbInitialized); manager.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, heightfieldShape, guard.get()); manager.addWater(cellPosition, ESM::Land::REAL_SIZE, -1, guard.get()); } else { if ((cell.mData.mFlags & ESM::Cell::HasWater) != 0) manager.addWater(cellPosition, std::numeric_limits::max(), cell.mWater, guard.get()); } forEachObject( cell, esmData, vfs, bulletShapeManager, readers, [&](BulletObject object, const CellRef& cellRef) { if (object.getShapeInstance()->mVisualCollisionType != Resource::VisualCollisionType::None) return; const btTransform& transform = object.getCollisionObject().getWorldTransform(); const btAABB aabb = BulletHelpers::getAabb(*object.getCollisionObject().getCollisionShape(), transform); mergeOrAssign(aabb, data.mAabb, data.mAabbInitialized); if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) data.mAabb.merge(BulletHelpers::getAabb(*avoid, transform)); const ObjectId objectId(++objectsCounter); const CollisionShape shape(object.getShapeInstance(), *object.getCollisionObject().getCollisionShape(), object.getObjectTransform()); if (!manager.addObject(objectId, shape, transform, DetourNavigator::AreaType_ground, guard.get())) throw std::logic_error( makeAddObjectErrorMessage(objectId, DetourNavigator::AreaType_ground, shape)); addedCellRefs.push_back(AddedCellRef{ .mCell = cell.getDescription(), .mCellRef = cellRef, .mRange = makeTilesPositionsRange(shape.getShape(), transform, settings.mRecast), }); if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) { const ObjectId avoidObjectId(++objectsCounter); const CollisionShape avoidShape(object.getShapeInstance(), *avoid, object.getObjectTransform()); if (!manager.addObject( avoidObjectId, avoidShape, transform, DetourNavigator::AreaType_null, guard.get())) throw std::logic_error( makeAddObjectErrorMessage(avoidObjectId, DetourNavigator::AreaType_null, avoidShape)); } data.mObjects.emplace_back(std::move(object)); }); if (writeBinaryLog) serializeToStderr(ProcessedCells{ static_cast(i + 1) }); Log(Debug::Info) << "Processed " << (exterior ? "exterior" : "interior") << " cell (" << (i + 1) << "/" << cells.size() << ") " << cell.getDescription() << " with " << (data.mObjects.size() - cellObjectsBegin) << " objects"; } const std::map changedTiles = manager.takeChangedTiles(guard.get()); if (worldspace != ESM::Cell::sDefaultWorldspaceId) detectDisconnectedTileGroups(worldspace, changedTiles, addedCellRefs); data.mTiles.reserve(changedTiles.size()); std::ranges::transform(changedTiles, std::back_inserter(data.mTiles), [](const auto& v) { return v.first; }); Log(Debug::Info) << "Processed " << cells.size() << " cells, added " << data.mObjects.size() << " objects and " << data.mHeightfields.size() << " height fields"; return data; } }