diff options
Diffstat (limited to 'src/common')
17 files changed, 2199 insertions, 101 deletions
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 8b8ecc0f471..4aef665cc6f 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -15,6 +15,7 @@ CollectSourceFiles( ${CMAKE_CURRENT_SOURCE_DIR}/Debugging/Windows ${CMAKE_CURRENT_SOURCE_DIR}/Platform ${CMAKE_CURRENT_SOURCE_DIR}/PrecompiledHeaders + ${CMAKE_CURRENT_SOURCE_DIR}/mmaps_common ${CMAKE_CURRENT_SOURCE_DIR}/network) if(WIN32) @@ -47,6 +48,7 @@ CollectIncludeDirectories( PUBLIC_INCLUDES # Exclude ${CMAKE_CURRENT_SOURCE_DIR}/PrecompiledHeaders + ${CMAKE_CURRENT_SOURCE_DIR}/mmaps_common ${CMAKE_CURRENT_SOURCE_DIR}/network) target_include_directories(common @@ -65,7 +67,6 @@ target_link_libraries(common boost fmt g3dlib - Detour sfmt utf8cpp openssl @@ -104,4 +105,5 @@ unset(PRIVATE_SOURCES) unset(PRIVATE_PCH_HEADER) unset(PUBLIC_INCLUDES) +add_subdirectory(mmaps_common) add_subdirectory(network) diff --git a/src/common/Collision/BoundingIntervalHierarchyWrapper.h b/src/common/Collision/BoundingIntervalHierarchyWrapper.h index 85048da58bc..03c6f0268c8 100644 --- a/src/common/Collision/BoundingIntervalHierarchyWrapper.h +++ b/src/common/Collision/BoundingIntervalHierarchyWrapper.h @@ -19,6 +19,7 @@ #define TRINITYCORE_BOUNDING_INTERVAL_HIERARCHY_WRAPPER_H #include "BoundingIntervalHierarchy.h" +#include <span> #include <unordered_map> template<class T, class BoundsFunc = BoundsTrait<T> > @@ -115,6 +116,8 @@ public: MDLCallback<IsectCallback> callback(intersectCallback, m_objects.data(), m_objects.size()); m_tree.intersectPoint(point, callback); } + + std::span<T const* const> getObjects() const { return m_objects; } }; #endif // TRINITYCORE_BOUNDING_INTERVAL_HIERARCHY_WRAPPER_H diff --git a/src/common/Collision/DynamicTree.cpp b/src/common/Collision/DynamicTree.cpp index 85dc9d52618..129c1a41878 100644 --- a/src/common/Collision/DynamicTree.cpp +++ b/src/common/Collision/DynamicTree.cpp @@ -36,7 +36,7 @@ int CHECK_TREE_PERIOD = 200; } // namespace template<> struct PositionTrait< GameObjectModel> { - static void getPosition(GameObjectModel const& g, G3D::Vector3& p) { p = g.getPosition(); } + static void getPosition(GameObjectModel const& g, G3D::Vector3& p) { p = g.GetPosition(); } }; template<> struct BoundsTrait< GameObjectModel> { @@ -280,3 +280,9 @@ bool DynamicMapTree::getAreaAndLiquidData(float x, float y, float z, PhaseShift } return false; } + +std::span<GameObjectModel const* const> DynamicMapTree::getModelsInGrid(uint32 gx, uint32 gy) const +{ + // convert from map tile X/Y to RegularGrid internal representation + return impl->getObjects(63 - int32(gx), 63 - int32(gy)); +} diff --git a/src/common/Collision/DynamicTree.h b/src/common/Collision/DynamicTree.h index 11d701ebd7c..73c667acb29 100644 --- a/src/common/Collision/DynamicTree.h +++ b/src/common/Collision/DynamicTree.h @@ -21,6 +21,7 @@ #include "Define.h" #include "Optional.h" #include <memory> +#include <span> namespace G3D { @@ -59,6 +60,8 @@ public: void balance(); void update(uint32 diff); + + std::span<GameObjectModel const* const> getModelsInGrid(uint32 gx, uint32 gy) const; }; #endif // _DYNTREE_H diff --git a/src/common/Collision/Models/GameObjectModel.h b/src/common/Collision/Models/GameObjectModel.h index 2ea99f61e20..f82c1f6860c 100644 --- a/src/common/Collision/Models/GameObjectModel.h +++ b/src/common/Collision/Models/GameObjectModel.h @@ -21,15 +21,11 @@ #include "Define.h" #include <G3D/AABox.h> #include <G3D/Matrix3.h> +#include <G3D/Quat.h> #include <G3D/Ray.h> #include <G3D/Vector3.h> #include <memory> -namespace G3D -{ -class Quat; -} - namespace VMAP { class WorldModel; @@ -53,25 +49,33 @@ public: virtual bool IsInPhase(PhaseShift const& /*phaseShift*/) const = 0; virtual G3D::Vector3 GetPosition() const = 0; virtual G3D::Quat GetRotation() const = 0; + virtual int64 GetPackedRotation() const = 0; virtual float GetScale() const = 0; virtual void DebugVisualizeCorner(G3D::Vector3 const& /*corner*/) const = 0; }; class TC_COMMON_API GameObjectModel /*, public Intersectable*/ { - GameObjectModel() : iCollisionEnabled(false), iLosBlockingDisabled(false), iInvScale(0), iScale(0), iModel(nullptr) { } + GameObjectModel() : iCollisionEnabled(false), iLosBlockingDisabled(false), iIncludeInNavMesh(false), iInvScale(0), iScale(0), iModel(nullptr) { } public: const G3D::AABox& getBounds() const { return iBound; } ~GameObjectModel(); - const G3D::Vector3& getPosition() const { return iPos;} + uint32 GetDisplayId() const { return owner->GetDisplayId(); } + G3D::Vector3 const& GetPosition() const { return iPos; } + G3D::Quat GetRotation() const { return owner->GetRotation(); } + G3D::Matrix3 const& GetInvRot() const { return iInvRot; } + int64 GetPackedRotation() const { return owner->GetPackedRotation(); } + float GetScale() const { return iScale; } /* Enables/disables collision */ void EnableCollision(bool enable) { iCollisionEnabled = enable; } bool IsCollisionEnabled() const { return iCollisionEnabled; } void DisableLosBlocking(bool enable) { iLosBlockingDisabled = enable; } bool IsLosBlockingDisabled() const { return iLosBlockingDisabled; } + void IncludeInNavMesh(bool enable) { iIncludeInNavMesh = enable; } + bool IsIncludedInNavMesh() const { return iIncludeInNavMesh; } bool IsMapObject() const; uint8 GetNameSetId() const { return owner->GetNameSetId(); } @@ -83,11 +87,14 @@ public: bool UpdatePosition(); + std::shared_ptr<VMAP::WorldModel const> GetWorldModel() const { return iModel; } + private: bool initialize(std::unique_ptr<GameObjectModelOwnerBase> modelOwner, std::string const& dataPath); bool iCollisionEnabled; ///< Is model ignored in all checks bool iLosBlockingDisabled; ///< Is model ignored during line of sight checks (but is always included in location/height checks) + bool iIncludeInNavMesh; ///< Is model included when generating navigation mesh G3D::AABox iBound; G3D::Matrix3 iInvRot; G3D::Vector3 iPos; diff --git a/src/common/Collision/RegularGrid.h b/src/common/Collision/RegularGrid.h index 459388d2662..94f97b2cb5a 100644 --- a/src/common/Collision/RegularGrid.h +++ b/src/common/Collision/RegularGrid.h @@ -210,6 +210,13 @@ public: if (Node* node = nodes[cell.x][cell.y].get()) node->intersectRay(ray, intersectCallback, max_dist); } + + std::span<T const* const> getObjects(int x, int y) const + { + if (Node* n = nodes[x][y].get()) + return n->getObjects(); + return {}; + } }; #undef CELL_SIZE diff --git a/src/common/Define.h b/src/common/Define.h index f918db84314..32862f71530 100644 --- a/src/common/Define.h +++ b/src/common/Define.h @@ -129,6 +129,12 @@ # define TC_GAME_API TC_API_IMPORT #endif +#ifdef TRINITY_API_EXPORT_MMAPS_COMMON +# define TC_MMAPS_COMMON_API TC_API_EXPORT +#else +# define TC_MMAPS_COMMON_API TC_API_IMPORT +#endif + #define UI64FMTD "%" PRIu64 #define UI64LIT(N) UINT64_C(N) diff --git a/src/common/mmaps_common/CMakeLists.txt b/src/common/mmaps_common/CMakeLists.txt new file mode 100644 index 00000000000..2a65e8d5963 --- /dev/null +++ b/src/common/mmaps_common/CMakeLists.txt @@ -0,0 +1,52 @@ +# This file is part of the TrinityCore Project. See AUTHORS file for Copyright information +# +# This file is free software; as a special exception the author gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +CollectSourceFiles( + ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE_SOURCES) + +GroupSources(${CMAKE_CURRENT_SOURCE_DIR}) + +CollectIncludeDirectories( + ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC_INCLUDES +) + +add_library(mmaps_common ${PRIVATE_SOURCES}) + +target_link_libraries(mmaps_common + PRIVATE + trinity-core-interface + PUBLIC + common + Recast + Detour) + +target_include_directories(mmaps_common + PUBLIC + ${PUBLIC_INCLUDES}) + +set_target_properties(mmaps_common + PROPERTIES + COMPILE_WARNING_AS_ERROR ${WITH_WARNINGS_AS_ERRORS} + DEFINE_SYMBOL TRINITY_API_EXPORT_MMAPS_COMMON + FOLDER "tools") + +if(BUILD_SHARED_LIBS) + if(UNIX) + install(TARGETS mmaps_common + LIBRARY + DESTINATION lib) + elseif(WIN32) + install(TARGETS mmaps_common + RUNTIME + DESTINATION "${CMAKE_INSTALL_PREFIX}") + endif() +endif() diff --git a/src/common/mmaps_common/Generator/IntermediateValues.cpp b/src/common/mmaps_common/Generator/IntermediateValues.cpp new file mode 100644 index 00000000000..858ac25d418 --- /dev/null +++ b/src/common/mmaps_common/Generator/IntermediateValues.cpp @@ -0,0 +1,252 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "IntermediateValues.h" +#include "Log.h" +#include "Memory.h" +#include "StringFormat.h" + +namespace MMAP +{ + IntermediateValues::~IntermediateValues() + { + rcFreeCompactHeightfield(compactHeightfield); + rcFreeHeightField(heightfield); + rcFreeContourSet(contours); + rcFreePolyMesh(polyMesh); + rcFreePolyMeshDetail(polyMeshDetail); + } + + void IntermediateValues::writeIV(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY) + { + TC_LOG_INFO("maps.mmapgen.debug", "[Map {:04}] [{:02},{:02}]: Writing debug output intermediate values...", mapID, tileX, tileY); + + auto debugWrite = [=, outputDirectory = outputDirectory.generic_string()](char const* extension, auto const* data) + { + std::string fileName = Trinity::StringFormat("{}/meshes/{:04}_{:02}_{:02}{}.{}", outputDirectory, mapID, tileX, tileY, fileNameSuffix, extension); + if (auto file = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(fileName.c_str(), "wb"))) + { + IntermediateValues::debugWrite(file.get(), data); + } + else + TC_LOG_ERROR("maps.mmapgen.debug", "{}: [{:04}-{:02},{:02}] Failed to open {} for writing!", strerror(errno), mapID, tileX, tileY, fileName); + }; + + if (heightfield) + debugWrite("hf", heightfield); + if (compactHeightfield) + debugWrite("chf", compactHeightfield); + if (contours) + debugWrite("cs", contours); + if (polyMesh) + debugWrite("pmesh", polyMesh); + if (polyMeshDetail) + debugWrite("dmesh", polyMeshDetail); + } + + void IntermediateValues::debugWrite(FILE* file, rcHeightfield const* mesh) + { + if (!file || !mesh) + return; + + fwrite(&(mesh->cs), sizeof(float), 1, file); + fwrite(&(mesh->ch), sizeof(float), 1, file); + fwrite(&(mesh->width), sizeof(int), 1, file); + fwrite(&(mesh->height), sizeof(int), 1, file); + fwrite(mesh->bmin, sizeof(float), 3, file); + fwrite(mesh->bmax, sizeof(float), 3, file); + + for (int y = 0; y < mesh->height; ++y) + for (int x = 0; x < mesh->width; ++x) + { + rcSpan* span = mesh->spans[x+y*mesh->width]; + + // first, count the number of spans + int spanCount = 0; + while (span) + { + spanCount++; + span = span->next; + } + + // write the span count + fwrite(&spanCount, sizeof(int), 1, file); + + // write the spans + span = mesh->spans[x+y*mesh->width]; + while (span) + { + fwrite(span, sizeof(rcSpan), 1, file); + span = span->next; + } + } + } + + void IntermediateValues::debugWrite(FILE* file, rcCompactHeightfield const* chf) + { + if (!file | !chf) + return; + + fwrite(&(chf->width), sizeof(chf->width), 1, file); + fwrite(&(chf->height), sizeof(chf->height), 1, file); + fwrite(&(chf->spanCount), sizeof(chf->spanCount), 1, file); + + fwrite(&(chf->walkableHeight), sizeof(chf->walkableHeight), 1, file); + fwrite(&(chf->walkableClimb), sizeof(chf->walkableClimb), 1, file); + + fwrite(&(chf->maxDistance), sizeof(chf->maxDistance), 1, file); + fwrite(&(chf->maxRegions), sizeof(chf->maxRegions), 1, file); + + fwrite(chf->bmin, sizeof(chf->bmin), 1, file); + fwrite(chf->bmax, sizeof(chf->bmax), 1, file); + + fwrite(&(chf->cs), sizeof(chf->cs), 1, file); + fwrite(&(chf->ch), sizeof(chf->ch), 1, file); + + int tmp = 0; + if (chf->cells) tmp |= 1; + if (chf->spans) tmp |= 2; + if (chf->dist) tmp |= 4; + if (chf->areas) tmp |= 8; + + fwrite(&tmp, sizeof(tmp), 1, file); + + if (chf->cells) + fwrite(chf->cells, sizeof(rcCompactCell), chf->width*chf->height, file); + if (chf->spans) + fwrite(chf->spans, sizeof(rcCompactSpan), chf->spanCount, file); + if (chf->dist) + fwrite(chf->dist, sizeof(unsigned short), chf->spanCount, file); + if (chf->areas) + fwrite(chf->areas, sizeof(unsigned char), chf->spanCount, file); + } + + void IntermediateValues::debugWrite(FILE* file, rcContourSet const* cs) + { + if (!file || !cs) + return; + + fwrite(&(cs->cs), sizeof(float), 1, file); + fwrite(&(cs->ch), sizeof(float), 1, file); + fwrite(cs->bmin, sizeof(float), 3, file); + fwrite(cs->bmax, sizeof(float), 3, file); + fwrite(&(cs->nconts), sizeof(int), 1, file); + for (int i = 0; i < cs->nconts; ++i) + { + fwrite(&cs->conts[i].area, sizeof(unsigned char), 1, file); + fwrite(&cs->conts[i].reg, sizeof(unsigned short), 1, file); + fwrite(&cs->conts[i].nverts, sizeof(int), 1, file); + fwrite(cs->conts[i].verts, sizeof(int), cs->conts[i].nverts*4, file); + fwrite(&cs->conts[i].nrverts, sizeof(int), 1, file); + fwrite(cs->conts[i].rverts, sizeof(int), cs->conts[i].nrverts*4, file); + } + } + + void IntermediateValues::debugWrite(FILE* file, rcPolyMesh const* mesh) + { + if (!file || !mesh) + return; + + fwrite(&(mesh->cs), sizeof(float), 1, file); + fwrite(&(mesh->ch), sizeof(float), 1, file); + fwrite(&(mesh->nvp), sizeof(int), 1, file); + fwrite(mesh->bmin, sizeof(float), 3, file); + fwrite(mesh->bmax, sizeof(float), 3, file); + fwrite(&(mesh->nverts), sizeof(int), 1, file); + fwrite(mesh->verts, sizeof(unsigned short), mesh->nverts*3, file); + fwrite(&(mesh->npolys), sizeof(int), 1, file); + fwrite(mesh->polys, sizeof(unsigned short), mesh->npolys*mesh->nvp*2, file); + fwrite(mesh->flags, sizeof(unsigned short), mesh->npolys, file); + fwrite(mesh->areas, sizeof(unsigned char), mesh->npolys, file); + fwrite(mesh->regs, sizeof(unsigned short), mesh->npolys, file); + } + + void IntermediateValues::debugWrite(FILE* file, rcPolyMeshDetail const* mesh) + { + if (!file || !mesh) + return; + + fwrite(&(mesh->nverts), sizeof(int), 1, file); + fwrite(mesh->verts, sizeof(float), mesh->nverts*3, file); + fwrite(&(mesh->ntris), sizeof(int), 1, file); + fwrite(mesh->tris, sizeof(char), mesh->ntris*4, file); + fwrite(&(mesh->nmeshes), sizeof(int), 1, file); + fwrite(mesh->meshes, sizeof(int), mesh->nmeshes*4, file); + } + + void IntermediateValues::generateObjFile(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY, MeshData const& meshData) + { + std::string objFileName; + objFileName = Trinity::StringFormat("{}/meshes/map{:04}_{:02}_{:02}{}.obj", outputDirectory.generic_string(), mapID, tileX, tileY, fileNameSuffix); + + auto objFile = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(objFileName.c_str(), "wb")); + if (!objFile) + { + TC_LOG_ERROR("maps.mmapgen.debug", "{}: Failed to open {} for writing!", strerror(errno), objFileName); + return; + } + + std::vector<float> allVerts; + std::vector<int> allTris; + + allTris.insert(allTris.end(), meshData.liquidTris.begin(), meshData.liquidTris.end()); + allVerts.insert(allVerts.end(), meshData.liquidVerts.begin(), meshData.liquidVerts.end()); + TerrainBuilder::copyIndices(meshData.solidTris, allTris, allVerts.size() / 3); + allVerts.insert(allVerts.end(), meshData.solidVerts.begin(), meshData.solidVerts.end()); + + float* verts = allVerts.data(); + int vertCount = allVerts.size() / 3; + int* tris = allTris.data(); + int triCount = allTris.size() / 3; + + for (std::size_t i = 0; i < allVerts.size() / 3; i++) + fprintf(objFile.get(), "v %f %f %f\n", verts[i * 3], verts[i * 3 + 1], verts[i * 3 + 2]); + + for (std::size_t i = 0; i < allTris.size() / 3; i++) + fprintf(objFile.get(), "f %i %i %i\n", tris[i * 3] + 1, tris[i * 3 + 1] + 1, tris[i * 3 + 2] + 1); + + TC_LOG_INFO("maps.mmapgen.debug", "[Map {:04}] [{:02},{:02}]: Writing debug output object file...", mapID, tileX, tileY); + + objFileName = Trinity::StringFormat("{}/meshes/map{:04}.map", outputDirectory.generic_string(), mapID); + + objFile.reset(fopen(objFileName.c_str(), "wb")); + if (!objFile) + { + TC_LOG_ERROR("maps.mmapgen.debug", "{}: Failed to open {} for writing!", strerror(errno), objFileName); + return; + } + + char b = '\0'; + fwrite(&b, sizeof(char), 1, objFile.get()); + + objFileName = Trinity::StringFormat("{}/meshes/map{:04}_{:02}_{:02}{}.mesh", outputDirectory.generic_string(), mapID, tileX, tileY, fileNameSuffix); + objFile.reset(fopen(objFileName.c_str(), "wb")); + if (!objFile) + { + TC_LOG_ERROR("maps.mmapgen.debug", "{}: Failed to open {} for writing!", strerror(errno), objFileName); + return; + } + + fwrite(&vertCount, sizeof(int), 1, objFile.get()); + fwrite(verts, sizeof(float), vertCount*3, objFile.get()); + fflush(objFile.get()); + + fwrite(&triCount, sizeof(int), 1, objFile.get()); + fwrite(tris, sizeof(int), triCount*3, objFile.get()); + fflush(objFile.get()); + } +} diff --git a/src/common/mmaps_common/Generator/IntermediateValues.h b/src/common/mmaps_common/Generator/IntermediateValues.h new file mode 100644 index 00000000000..f8578683e61 --- /dev/null +++ b/src/common/mmaps_common/Generator/IntermediateValues.h @@ -0,0 +1,78 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _INTERMEDIATE_VALUES_H +#define _INTERMEDIATE_VALUES_H + +#include "TerrainBuilder.h" +#include <Recast.h> +#include <utility> + +namespace MMAP +{ + // this class gathers all debug info holding and output + struct IntermediateValues + { + rcHeightfield* heightfield; + rcCompactHeightfield* compactHeightfield; + rcContourSet* contours; + rcPolyMesh* polyMesh; + rcPolyMeshDetail* polyMeshDetail; + + IntermediateValues() : heightfield(nullptr), compactHeightfield(nullptr), + contours(nullptr), polyMesh(nullptr), polyMeshDetail(nullptr) {} + + IntermediateValues(IntermediateValues const&) = delete; + + IntermediateValues(IntermediateValues&& other) noexcept : + heightfield(std::exchange(other.heightfield, nullptr)), + compactHeightfield(std::exchange(other.compactHeightfield, nullptr)), + contours(std::exchange(other.contours, nullptr)), + polyMesh(std::exchange(other.polyMesh, nullptr)), + polyMeshDetail(std::exchange(other.polyMeshDetail, nullptr)) + { + } + + ~IntermediateValues(); + + IntermediateValues& operator=(IntermediateValues const&) = delete; + + IntermediateValues& operator=(IntermediateValues&& other) noexcept + { + if (this != std::addressof(other)) + { + heightfield = std::exchange(other.heightfield, nullptr); + compactHeightfield = std::exchange(other.compactHeightfield, nullptr); + contours = std::exchange(other.contours, nullptr); + polyMesh = std::exchange(other.polyMesh, nullptr); + polyMeshDetail = std::exchange(other.polyMeshDetail, nullptr); + } + return *this; + } + + void writeIV(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY); + + static void debugWrite(FILE* file, rcHeightfield const* mesh); + static void debugWrite(FILE* file, rcCompactHeightfield const* chf); + static void debugWrite(FILE* file, rcContourSet const* cs); + static void debugWrite(FILE* file, rcPolyMesh const* mesh); + static void debugWrite(FILE* file, rcPolyMeshDetail const* mesh); + + void generateObjFile(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY, MeshData const& meshData); + }; +} +#endif diff --git a/src/common/mmaps_common/Generator/TerrainBuilder.cpp b/src/common/mmaps_common/Generator/TerrainBuilder.cpp new file mode 100644 index 00000000000..449a5cb7557 --- /dev/null +++ b/src/common/mmaps_common/Generator/TerrainBuilder.cpp @@ -0,0 +1,812 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "TerrainBuilder.h" +#include "Log.h" +#include "MapDefines.h" +#include "MapTree.h" +#include "MMapDefines.h" +#include "Memory.h" +#include "ModelInstance.h" +#include "StringFormat.h" +#include "Util.h" +#include "VMapManager.h" +#include <unordered_map> + +namespace MMAP +{ + std::unique_ptr<VMAP::VMapManager> (*CreateVMapManager)(uint32 mapId); + + TerrainBuilder::TerrainBuilder(boost::filesystem::path const& inputDirectory, bool skipLiquid) : + m_inputDirectory(inputDirectory), + m_skipLiquid (skipLiquid) + { + } + + /**************************************************************************/ + void TerrainBuilder::getLoopVars(Spot portion, int& loopStart, int& loopEnd, int& loopInc) + { + switch (portion) + { + case ENTIRE: + loopStart = 0; + loopEnd = V8_SIZE_SQ; + loopInc = 1; + break; + case TOP: + loopStart = 0; + loopEnd = V8_SIZE; + loopInc = 1; + break; + case LEFT: + loopStart = 0; + loopEnd = V8_SIZE_SQ - V8_SIZE + 1; + loopInc = V8_SIZE; + break; + case RIGHT: + loopStart = V8_SIZE - 1; + loopEnd = V8_SIZE_SQ; + loopInc = V8_SIZE; + break; + case BOTTOM: + loopStart = V8_SIZE_SQ - V8_SIZE; + loopEnd = V8_SIZE_SQ; + loopInc = 1; + break; + } + } + + /**************************************************************************/ + void TerrainBuilder::loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager) + { + if (loadMap(mapID, tileX, tileY, meshData, vmapManager, ENTIRE)) + { + loadMap(mapID, tileX, tileY+1, meshData, vmapManager, LEFT); + loadMap(mapID, tileX, tileY-1, meshData, vmapManager, RIGHT); + loadMap(mapID, tileX+1, tileY, meshData, vmapManager, TOP); + loadMap(mapID, tileX-1, tileY, meshData, vmapManager, BOTTOM); + } + } + + /**************************************************************************/ + bool TerrainBuilder::loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager, Spot portion) + { + std::string mapFileName = Trinity::StringFormat("{}/maps/{:04}_{:02}_{:02}.map", m_inputDirectory.generic_string(), mapID, tileX, tileY); + + auto mapFile = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(mapFileName.c_str(), "rb")); + if (!mapFile) + { + int32 parentMapId = vmapManager->getParentMapId(mapID); + while (!mapFile && parentMapId != -1) + { + mapFileName = Trinity::StringFormat("{}/maps/{:04}_{:02}_{:02}.map", m_inputDirectory.generic_string(), parentMapId, tileX, tileY); + mapFile.reset(fopen(mapFileName.c_str(), "rb")); + parentMapId = vmapManager->getParentMapId(parentMapId); + } + } + + if (!mapFile) + return false; + + map_fileheader fheader; + if (fread(&fheader, sizeof(map_fileheader), 1, mapFile.get()) != 1 || + fheader.versionMagic != MapVersionMagic) + { + TC_LOG_ERROR("maps.mmapgen", "{} is the wrong version, please extract new .map files", mapFileName); + return false; + } + + map_heightHeader hheader; + fseek(mapFile.get(), fheader.heightMapOffset, SEEK_SET); + + bool haveTerrain = false; + bool haveLiquid = false; + if (fread(&hheader, sizeof(map_heightHeader), 1, mapFile.get()) == 1) + { + haveTerrain = !hheader.flags.HasFlag(map_heightHeaderFlags::NoHeight); + haveLiquid = fheader.liquidMapOffset && !m_skipLiquid; + } + + // no data in this map file + if (!haveTerrain && !haveLiquid) + return false; + + // data used later + uint8 holes[16][16][8] = { }; + uint16 liquid_entry[16][16] = { }; + map_liquidHeaderTypeFlags liquid_flags[16][16] = { }; + std::vector<int> ltriangles; + std::vector<int> ttriangles; + + // terrain data + if (haveTerrain) + { + float heightMultiplier; + float V9[V9_SIZE_SQ], V8[V8_SIZE_SQ]; + size_t expected = V9_SIZE_SQ + V8_SIZE_SQ; + + if (hheader.flags.HasFlag(map_heightHeaderFlags::HeightAsInt8)) + { + uint8 v9[V9_SIZE_SQ]; + uint8 v8[V8_SIZE_SQ]; + size_t count = 0; + count += fread(v9, sizeof(uint8), V9_SIZE_SQ, mapFile.get()); + count += fread(v8, sizeof(uint8), V8_SIZE_SQ, mapFile.get()); + if (count != expected) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} height data expected {}, read {}", mapFileName, expected, count); + + heightMultiplier = (hheader.gridMaxHeight - hheader.gridHeight) / 255; + + for (int i = 0; i < V9_SIZE_SQ; ++i) + V9[i] = (float)v9[i]*heightMultiplier + hheader.gridHeight; + + for (int i = 0; i < V8_SIZE_SQ; ++i) + V8[i] = (float)v8[i]*heightMultiplier + hheader.gridHeight; + } + else if (hheader.flags.HasFlag(map_heightHeaderFlags::HeightAsInt16)) + { + uint16 v9[V9_SIZE_SQ]; + uint16 v8[V8_SIZE_SQ]; + size_t count = 0; + count += fread(v9, sizeof(uint16), V9_SIZE_SQ, mapFile.get()); + count += fread(v8, sizeof(uint16), V8_SIZE_SQ, mapFile.get()); + if (count != expected) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} height data expected {}, read {}", mapFileName, expected, count); + + heightMultiplier = (hheader.gridMaxHeight - hheader.gridHeight) / 65535; + + for (int i = 0; i < V9_SIZE_SQ; ++i) + V9[i] = (float)v9[i]*heightMultiplier + hheader.gridHeight; + + for (int i = 0; i < V8_SIZE_SQ; ++i) + V8[i] = (float)v8[i]*heightMultiplier + hheader.gridHeight; + } + else + { + size_t count = 0; + count += fread(V9, sizeof(float), V9_SIZE_SQ, mapFile.get()); + count += fread(V8, sizeof(float), V8_SIZE_SQ, mapFile.get()); + if (count != expected) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} height data expected {}, read {}", mapFileName, expected, count); + } + + // hole data + if (fheader.holesSize != 0) + { + memset(holes, 0, fheader.holesSize); + fseek(mapFile.get(), fheader.holesOffset, SEEK_SET); + if (fread(holes, fheader.holesSize, 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} holes data expected {}, read {}", mapFileName, 1, 0); + } + + int count = meshData.solidVerts.size() / 3; + meshData.solidVerts.resize(meshData.solidVerts.size() + (V9_SIZE_SQ + V8_SIZE_SQ) * 3); + float* solidVerts = meshData.solidVerts.data() + count * 3; + + float xoffset = (float(tileX)-32)*GRID_SIZE; + float yoffset = (float(tileY)-32)*GRID_SIZE; + + for (int i = 0; i < V9_SIZE_SQ; ++i) + { + getHeightCoord(i, GRID_V9, xoffset, yoffset, solidVerts, V9); + solidVerts += 3; + } + + for (int i = 0; i < V8_SIZE_SQ; ++i) + { + getHeightCoord(i, GRID_V8, xoffset, yoffset, solidVerts, V8); + solidVerts += 3; + } + + int loopStart = 0, loopEnd = 0, loopInc = 0; + getLoopVars(portion, loopStart, loopEnd, loopInc); + for (int i = loopStart; i < loopEnd; i+=loopInc) + { + std::size_t trianglesOffset = ttriangles.size(); + ttriangles.resize(ttriangles.size() + 12); + getHeightTriangle(i, TOP, ttriangles.data() + trianglesOffset + 0, count); + getHeightTriangle(i, RIGHT, ttriangles.data() + trianglesOffset + 3, count); + getHeightTriangle(i, LEFT, ttriangles.data() + trianglesOffset + 6, count); + getHeightTriangle(i, BOTTOM, ttriangles.data() + trianglesOffset + 9, count); + } + } + + // liquid data + if (haveLiquid) + { + map_liquidHeader lheader; + fseek(mapFile.get(), fheader.liquidMapOffset, SEEK_SET); + if (fread(&lheader, sizeof(map_liquidHeader), 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid header expected {}, read {}", mapFileName, 1, 0); + + std::unique_ptr<float[]> liquid_map; + + if (!lheader.flags.HasFlag(map_liquidHeaderFlags::NoType)) + { + if (fread(liquid_entry, sizeof(liquid_entry), 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid id expected {}, read {}", mapFileName, 1, 0); + if (fread(liquid_flags, sizeof(liquid_flags), 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid flags expected {}, read {}", mapFileName, 1, 0); + } + else + { + std::fill_n(&liquid_entry[0][0], 16 * 16, lheader.liquidType); + std::fill_n(&liquid_flags[0][0], 16 * 16, lheader.liquidFlags); + } + + if (!lheader.flags.HasFlag(map_liquidHeaderFlags::NoHeight)) + { + uint32 toRead = lheader.width * lheader.height; + liquid_map = std::make_unique<float[]>(toRead); + if (fread(liquid_map.get(), sizeof(float), toRead, mapFile.get()) != toRead) + { + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid header expected {}, read {}", mapFileName, toRead, 0); + liquid_map = nullptr; + } + } + + int count = meshData.liquidVerts.size() / 3; + meshData.liquidVerts.resize(meshData.liquidVerts.size() + V9_SIZE_SQ * 3); + float* liquidVerts = meshData.liquidVerts.data(); + float xoffset = (float(tileX)-32)*GRID_SIZE; + float yoffset = (float(tileY)-32)*GRID_SIZE; + + int row, col; + + // generate coordinates + if (!lheader.flags.HasFlag(map_liquidHeaderFlags::NoHeight)) + { + int j = 0; + for (int i = 0; i < V9_SIZE_SQ; ++i) + { + row = i / V9_SIZE; + col = i % V9_SIZE; + + if (row < lheader.offsetY || row >= lheader.offsetY + lheader.height || + col < lheader.offsetX || col >= lheader.offsetX + lheader.width) + { + // dummy vert using invalid height + liquidVerts[(count + i) * 3 + 0] = (yoffset + col * GRID_PART_SIZE) * -1; + liquidVerts[(count + i) * 3 + 1] = INVALID_MAP_LIQ_HEIGHT; + liquidVerts[(count + i) * 3 + 2] = (xoffset + row * GRID_PART_SIZE) * -1; + continue; + } + + getLiquidCoord(i, j, xoffset, yoffset, &liquidVerts[(count + i) * 3], liquid_map.get()); + j++; + } + } + else + { + for (int i = 0; i < V9_SIZE_SQ; ++i) + { + row = i / V9_SIZE; + col = i % V9_SIZE; + liquidVerts[(count + i) * 3 + 0] = (yoffset + col * GRID_PART_SIZE) * -1; + liquidVerts[(count + i) * 3 + 1] = lheader.liquidLevel; + liquidVerts[(count + i) * 3 + 2] = (xoffset + row * GRID_PART_SIZE) * -1; + } + } + + int loopStart = 0, loopEnd = 0, loopInc = 0; + getLoopVars(portion, loopStart, loopEnd, loopInc); + + // generate triangles + for (int i = loopStart; i < loopEnd; i += loopInc) + { + std::size_t trianglesOffset = ltriangles.size(); + ltriangles.resize(ltriangles.size() + 6); + getHeightTriangle(i, TOP, ltriangles.data() + trianglesOffset + 0, count, true); + getHeightTriangle(i, BOTTOM, ltriangles.data() + trianglesOffset + 3, count, true); + } + } + + // now that we have gathered the data, we can figure out which parts to keep: + // liquid above ground, ground above liquid + int loopStart = 0, loopEnd = 0, loopInc = 0, tTriCount = 4; + bool useTerrain, useLiquid; + + float* lverts = meshData.liquidVerts.data(); + int* ltris = ltriangles.data(); + + float* tverts = meshData.solidVerts.data(); + int* ttris = ttriangles.data(); + + if ((ltriangles.size() + ttriangles.size()) == 0) + return false; + + // make a copy of liquid vertices + // used to pad right-bottom frame due to lost vertex data at extraction + std::unique_ptr<float[]> lverts_copy; + if (!meshData.liquidVerts.empty()) + { + lverts_copy = std::make_unique<float[]>(meshData.liquidVerts.size()); + memcpy(lverts_copy.get(), lverts, sizeof(float)* meshData.liquidVerts.size()); + } + + getLoopVars(portion, loopStart, loopEnd, loopInc); + for (int i = loopStart; i < loopEnd; i+=loopInc) + { + for (int j = 0; j < 2; ++j) + { + // default is true, will change to false if needed + useTerrain = true; + useLiquid = true; + EnumFlag<map_liquidHeaderTypeFlags> liquidType = map_liquidHeaderTypeFlags::NoWater; + uint8 navLiquidType = NAV_AREA_EMPTY; + + // if there is no liquid, don't use liquid + if (meshData.liquidVerts.empty() || ltriangles.empty()) + useLiquid = false; + else + { + liquidType = getLiquidType(i, liquid_flags); + if (liquidType.HasFlag(map_liquidHeaderTypeFlags::DarkWater)) + { + // players should not be here, so logically neither should creatures + useTerrain = false; + useLiquid = false; + } + else if (liquidType.HasFlag(map_liquidHeaderTypeFlags::Water | map_liquidHeaderTypeFlags::Ocean)) + navLiquidType = NAV_AREA_WATER; + else if (liquidType.HasFlag(map_liquidHeaderTypeFlags::Magma | map_liquidHeaderTypeFlags::Slime)) + navLiquidType = NAV_AREA_MAGMA_SLIME; + else + useLiquid = false; + } + + // if there is no terrain, don't use terrain + if (ttriangles.empty()) + useTerrain = false; + + // while extracting ADT data we are losing right-bottom vertices + // this code adds fair approximation of lost data + if (useLiquid) + { + float quadHeight = 0; + uint32 validCount = 0; + for(uint32 idx = 0; idx < 3; idx++) + { + float h = lverts_copy[ltris[idx]*3 + 1]; + if (h != INVALID_MAP_LIQ_HEIGHT && h < INVALID_MAP_LIQ_HEIGHT_MAX) + { + quadHeight += h; + validCount++; + } + } + + // update vertex height data + if (validCount > 0 && validCount < 3) + { + quadHeight /= validCount; + for(uint32 idx = 0; idx < 3; idx++) + { + float h = lverts[ltris[idx]*3 + 1]; + if (h == INVALID_MAP_LIQ_HEIGHT || h > INVALID_MAP_LIQ_HEIGHT_MAX) + lverts[ltris[idx]*3 + 1] = quadHeight; + } + } + + // no valid vertexes - don't use this poly at all + if (validCount == 0) + useLiquid = false; + } + + // if there is a hole here, don't use the terrain + if (useTerrain && fheader.holesSize != 0) + useTerrain = !isHole(i, holes); + + // we use only one terrain kind per quad - pick higher one + if (useTerrain && useLiquid) + { + float minLLevel = INVALID_MAP_LIQ_HEIGHT_MAX; + float maxLLevel = INVALID_MAP_LIQ_HEIGHT; + for(uint32 x = 0; x < 3; x++) + { + float h = lverts[ltris[x]*3 + 1]; + if (minLLevel > h) + minLLevel = h; + + if (maxLLevel < h) + maxLLevel = h; + } + + float maxTLevel = INVALID_MAP_LIQ_HEIGHT; + float minTLevel = INVALID_MAP_LIQ_HEIGHT_MAX; + for(uint32 x = 0; x < 6; x++) + { + float h = tverts[ttris[x]*3 + 1]; + if (maxTLevel < h) + maxTLevel = h; + + if (minTLevel > h) + minTLevel = h; + } + + // terrain under the liquid? + if (minLLevel > maxTLevel) + useTerrain = false; + + //liquid under the terrain? + if (minTLevel > maxLLevel) + useLiquid = false; + } + + // store the result + if (useLiquid) + { + meshData.liquidTris.insert(meshData.liquidTris.end(), <ris[0], <ris[3]); + meshData.liquidType.push_back(navLiquidType); + } + + if (useTerrain) + meshData.solidTris.insert(meshData.solidTris.end(), &ttris[0], &ttris[3 * tTriCount / 2]); + + // advance to next set of triangles + ltris += 3; + ttris += 3*tTriCount/2; + } + } + + return !meshData.solidTris.empty() || !meshData.liquidTris.empty(); + } + + /**************************************************************************/ + inline void TerrainBuilder::getHeightCoord(int index, Grid grid, float xOffset, float yOffset, float* coord, float* v) + { + // wow coords: x, y, height + // coord is mirroed about the horizontal axes + switch (grid) + { + case GRID_V9: + coord[0] = (yOffset + (int)(index % V9_SIZE) * GRID_PART_SIZE) * -1.f; + coord[1] = v[index]; + coord[2] = (xOffset + (int)(index / (V9_SIZE)) * GRID_PART_SIZE) * -1.f; + break; + case GRID_V8: + coord[0] = (yOffset + (int)(index % V8_SIZE) * GRID_PART_SIZE + GRID_PART_SIZE / 2.f) * -1.f; + coord[1] = v[index]; + coord[2] = (xOffset + (int)(index / (V8_SIZE)) * GRID_PART_SIZE + GRID_PART_SIZE / 2.f) * -1.f; + break; + } + } + + /**************************************************************************/ + inline void TerrainBuilder::getHeightTriangle(int square, Spot triangle, int* indices, int offset, bool liquid/* = false*/) + { + int rowOffset = square/V8_SIZE; + if (!liquid) + switch (triangle) + { + case TOP: + indices[0] = V9_SIZE_SQ + square + offset; // 0-----1 .... 128 + indices[1] = square + 1 + rowOffset + offset; // |\ T /| + indices[2] = square + rowOffset + offset; // | \ / | + break; // |L 0 R| .. 127 + case LEFT: // | / \ | + indices[0] = square + V9_SIZE + rowOffset + offset; // |/ B \| + indices[1] = V9_SIZE_SQ + square + offset; // 129---130 ... 386 + indices[2] = square + rowOffset + offset; // |\ /| + break; // | \ / | + case RIGHT: // | 128 | .. 255 + indices[0] = V9_SIZE_SQ + square + offset; // | / \ | + indices[1] = square + V9_SIZE + 1 + rowOffset + offset; // |/ \| + indices[2] = square + 1 + rowOffset + offset; // 258---259 ... 515 + break; + case BOTTOM: + indices[0] = square + V9_SIZE + rowOffset + offset; + indices[1] = square + V9_SIZE + 1 + rowOffset + offset; + indices[2] = V9_SIZE_SQ + square + offset; + break; + default: break; + } + else + switch (triangle) + { // 0-----1 .... 128 + case TOP: // |\ | + indices[0] = square + V9_SIZE + 1 + rowOffset + offset; // | \ T | + indices[1] = square + 1 + rowOffset + offset; // | \ | + indices[2] = square + rowOffset + offset; // | B \ | + break; // | \| + case BOTTOM: // 129---130 ... 386 + indices[0] = square + V9_SIZE + rowOffset + offset; // |\ | + indices[1] = square + V9_SIZE + 1 + rowOffset + offset; // | \ | + indices[2] = square + rowOffset + offset; // | \ | + break; // | \ | + default: break; // | \| + } // 258---259 ... 515 + + } + + /**************************************************************************/ + inline void TerrainBuilder::getLiquidCoord(int index, int index2, float xOffset, float yOffset, float* coord, float* v) + { + // wow coords: x, y, height + // coord is mirroed about the horizontal axes + coord[0] = (yOffset + (int)(index % V9_SIZE) * GRID_PART_SIZE) * -1.f; + coord[1] = v[index2]; + coord[2] = (xOffset + (int)(index / (V9_SIZE)) * GRID_PART_SIZE) * -1.f; + } + + /**************************************************************************/ + inline bool TerrainBuilder::isHole(int square, uint8 const (&holes)[16][16][8]) + { + int row = square / 128; + int col = square % 128; + int cellRow = row / 8; // 8 squares per cell + int cellCol = col / 8; + int holeRow = row % 8; + int holeCol = col % 8; + + return (holes[cellRow][cellCol][holeRow] & (1 << holeCol)) != 0; + } + + /**************************************************************************/ + inline map_liquidHeaderTypeFlags TerrainBuilder::getLiquidType(int square, map_liquidHeaderTypeFlags const (&liquid_type)[16][16]) + { + int row = square / 128; + int col = square % 128; + int cellRow = row / 8; // 8 squares per cell + int cellCol = col / 8; + + return liquid_type[cellRow][cellCol]; + } + + /**************************************************************************/ + bool TerrainBuilder::loadVMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager) + { + VMAP::LoadResult result = vmapManager->loadMap((m_inputDirectory / "vmaps").string(), mapID, tileX, tileY); + if (result != VMAP::LoadResult::Success) + return false; + + auto vmapTile = Trinity::make_unique_ptr_with_deleter(vmapManager, [=](VMAP::VMapManager* mgr) + { + mgr->unloadMap(mapID, tileX, tileY); + }); + + std::span<VMAP::ModelInstance const> models = vmapManager->getModelsOnMap(mapID); + if (models.empty()) + return false; + + bool retval = false; + + for (VMAP::ModelInstance const& instance : models) + { + // model instances exist in tree even though there are instances of that model in this tile + VMAP::WorldModel const* worldModel = instance.getWorldModel(); + if (!worldModel) + continue; + + // now we have a model to add to the meshdata + retval = true; + + G3D::Vector3 position = instance.iPos; + position.x -= 32 * GRID_SIZE; + position.y -= 32 * GRID_SIZE; + loadVMapModel(worldModel, position, instance.GetInvRot(), instance.iScale, meshData, vmapManager); + } + + return retval; + } + + void TerrainBuilder::loadVMapModel(VMAP::WorldModel const* worldModel, G3D::Vector3 const& position, G3D::Matrix3 const& rotation, float scale, + MeshData& meshData, VMAP::VMapManager* vmapManager) + { + std::vector<VMAP::GroupModel> const& groupModels = worldModel->getGroupModels(); + + // all M2s need to have triangle indices reversed + bool isM2 = worldModel->IsM2(); + + // transform data + for (std::vector<VMAP::GroupModel>::const_iterator it = groupModels.begin(); it != groupModels.end(); ++it) + { + // first handle collision mesh + int offset = meshData.solidVerts.size() / 3; + transformVertices(it->GetVertices(), meshData.solidVerts, scale, rotation, position); + copyIndices(it->GetTriangles(), meshData.solidTris, offset, isM2); + + // now handle liquid data + VMAP::WmoLiquid const* liquid = it->GetLiquid(); + if (liquid && liquid->GetFlagsStorage()) + { + uint32 tilesX, tilesY; + G3D::Vector3 corner; + liquid->getPosInfo(tilesX, tilesY, corner); + uint32 vertsX = tilesX + 1; + uint32 vertsY = tilesY + 1; + uint8 const* flags = liquid->GetFlagsStorage(); + float const* data = liquid->GetHeightStorage(); + uint8 type = NAV_AREA_EMPTY; + + // convert liquid type to NavTerrain + EnumFlag<map_liquidHeaderTypeFlags> liquidFlags = map_liquidHeaderTypeFlags(vmapManager->GetLiquidFlagsPtr(liquid->GetType())); + if (liquidFlags.HasFlag(map_liquidHeaderTypeFlags::Water | map_liquidHeaderTypeFlags::Ocean)) + type = NAV_AREA_WATER; + else if (liquidFlags.HasFlag(map_liquidHeaderTypeFlags::Magma | map_liquidHeaderTypeFlags::Slime)) + type = NAV_AREA_MAGMA_SLIME; + + // indexing is weird... + // after a lot of trial and error, this is what works: + // vertex = y*vertsX+x + // tile = x*tilesY+y + // flag = y*tilesY+x + + uint32 liqOffset = meshData.liquidVerts.size() / 3; + meshData.liquidVerts.resize(meshData.liquidVerts.size() + vertsX * vertsY * 3); + float* liquidVerts = meshData.liquidVerts.data(); + for (uint32 x = 0; x < vertsX; ++x) + { + for (uint32 y = 0; y < vertsY; ++y) + { + G3D::Vector3 vert = G3D::Vector3(corner.x + x * GRID_PART_SIZE, corner.y + y * GRID_PART_SIZE, data[y * vertsX + x]); + vert = vert * rotation * scale + position; + vert.x *= -1.f; + vert.y *= -1.f; + liquidVerts[(liqOffset + x * vertsY + y) * 3 + 0] = vert.y; + liquidVerts[(liqOffset + x * vertsY + y) * 3 + 1] = vert.z; + liquidVerts[(liqOffset + x * vertsY + y) * 3 + 2] = vert.x; + } + } + + std::size_t liquidSquares = 0; + for (uint32 x = 0; x < tilesX; ++x) + { + for (uint32 y = 0; y < tilesY; ++y) + { + if ((flags[x + y * tilesX] & 0x0f) != 0x0f) + { + uint32 square = x * tilesY + y; + int idx1 = square + x; + int idx2 = square + 1 + x; + int idx3 = square + tilesY + 1 + 1 + x; + int idx4 = square + tilesY + 1 + x; + + std::size_t liquidTriOffset = meshData.liquidTris.size(); + meshData.liquidTris.resize(liquidTriOffset + 6); + int* liquidTris = meshData.liquidTris.data() + liquidTriOffset; + + // top triangle + liquidTris[0] = idx2 + liqOffset; + liquidTris[1] = idx1 + liqOffset; + liquidTris[2] = idx3 + liqOffset; + + // bottom triangle + liquidTris[3] = idx3 + liqOffset; + liquidTris[4] = idx1 + liqOffset; + liquidTris[5] = idx4 + liqOffset; + + ++liquidSquares; + } + } + } + + meshData.liquidType.resize(meshData.liquidType.size() + liquidSquares * 2, type); + } + } + } + + /**************************************************************************/ + void TerrainBuilder::transformVertices(std::vector<G3D::Vector3> const& source, std::vector<float>& dest, float scale, G3D::Matrix3 const& rotation, G3D::Vector3 const& position) + { + std::size_t offset = dest.size(); + dest.resize(dest.size() + source.size() * 3); + float* d = dest.data(); + for (std::size_t i = 0; i < source.size(); ++i) + { + // apply tranform, then mirror along the horizontal axes + G3D::Vector3 v(source[i] * rotation * scale + position); + v.x *= -1.f; + v.y *= -1.f; + d[offset + i * 3 + 0] = v.y; + d[offset + i * 3 + 1] = v.z; + d[offset + i * 3 + 2] = v.x; + } + } + + /**************************************************************************/ + void TerrainBuilder::copyIndices(std::vector<VMAP::MeshTriangle> const& source, std::vector<int>& dest, int offset, bool flip) + { + std::size_t destOffset = dest.size(); + dest.resize(dest.size() + source.size() * 3); + int* d = dest.data(); + if (flip) + { + for (VMAP::MeshTriangle const& triangle : source) + { + d[destOffset++] = triangle.idx2 + offset; + d[destOffset++] = triangle.idx1 + offset; + d[destOffset++] = triangle.idx0 + offset; + } + } + else + { + static_assert(sizeof(VMAP::MeshTriangle) == 3 * sizeof(uint32)); + std::ranges::transform(reinterpret_cast<uint32 const*>(source.data()), reinterpret_cast<uint32 const*>(source.data() + source.size()), + dest.data() + destOffset, [&](int src) { return src + offset; }); + } + } + + /**************************************************************************/ + void TerrainBuilder::copyIndices(std::vector<int> const& source, std::vector<int>& dest, int offset) + { + std::size_t destOffset = dest.size(); + dest.resize(destOffset + source.size()); + std::ranges::transform(source, dest.data() + destOffset, [&](int src) { return src + offset; }); + } + + /**************************************************************************/ + void TerrainBuilder::cleanVertices(std::vector<float>& verts, std::vector<int>& tris) + { + std::unordered_map<int, int> vertMap; + vertMap.reserve(tris.size()); + + int* t = tris.data(); + float* v = verts.data(); + + std::vector<float> cleanVerts; + cleanVerts.reserve(verts.size()); + int count = 0; + // collect all the vertex indices from triangle + for (std::size_t i = 0; i < tris.size(); ++i) + { + auto [vertItr, isNew] = vertMap.try_emplace(t[i], count); + if (!isNew) + continue; + + std::ptrdiff_t index = t[i]; + + cleanVerts.insert(cleanVerts.end(), &v[index * 3], &v[index * 3 + 3]); + count++; + } + + verts = std::move(cleanVerts); + + // update triangles to use new indices + for (std::size_t i = 0; i < tris.size(); ++i) + { + auto it = vertMap.find(t[i]); + if (it == vertMap.end()) + continue; + + t[i] = it->second; + } + } + + /**************************************************************************/ + void TerrainBuilder::loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY, MeshData &meshData, std::vector<OffMeshData> const& offMeshConnections) + { + for (OffMeshData const& offMeshConnection : offMeshConnections) + { + if (mapID != offMeshConnection.MapId || tileX != offMeshConnection.TileX || tileY != offMeshConnection.TileY) + continue; + + meshData.offMeshConnections.push_back(offMeshConnection.From[1]); + meshData.offMeshConnections.push_back(offMeshConnection.From[2]); + meshData.offMeshConnections.push_back(offMeshConnection.From[0]); + + meshData.offMeshConnections.push_back(offMeshConnection.To[1]); + meshData.offMeshConnections.push_back(offMeshConnection.To[2]); + meshData.offMeshConnections.push_back(offMeshConnection.To[0]); + + meshData.offMeshConnectionDirs.push_back(offMeshConnection.ConnectionFlags & OFFMESH_CONNECTION_FLAG_BIDIRECTIONAL); + meshData.offMeshConnectionRads.push_back(offMeshConnection.Radius); // agent size equivalent + // can be used same way as polygon flags + meshData.offMeshConnectionsAreas.push_back(offMeshConnection.AreaId); + meshData.offMeshConnectionsFlags.push_back(offMeshConnection.Flags); + } + } +} diff --git a/src/common/mmaps_common/Generator/TerrainBuilder.h b/src/common/mmaps_common/Generator/TerrainBuilder.h new file mode 100644 index 00000000000..a16d1669425 --- /dev/null +++ b/src/common/mmaps_common/Generator/TerrainBuilder.h @@ -0,0 +1,132 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _MMAP_TERRAIN_BUILDER_H +#define _MMAP_TERRAIN_BUILDER_H + +#include "MMapDefines.h" +#include "WorldModel.h" +#include <G3D/Vector3.h> +#include <boost/filesystem/path.hpp> + +namespace VMAP +{ +class VMapManager; +} + +enum class map_liquidHeaderTypeFlags : uint8; + +namespace MMAP +{ + enum Spot + { + TOP = 1, + RIGHT = 2, + LEFT = 3, + BOTTOM = 4, + ENTIRE = 5 + }; + + enum Grid + { + GRID_V8, + GRID_V9 + }; + + static const int V9_SIZE = 129; + static const int V9_SIZE_SQ = V9_SIZE*V9_SIZE; + static const int V8_SIZE = 128; + static const int V8_SIZE_SQ = V8_SIZE*V8_SIZE; + static const float GRID_SIZE = 533.3333f; + static const float GRID_PART_SIZE = GRID_SIZE/V8_SIZE; + + // see contrib/extractor/system.cpp, CONF_use_minHeight + static const float INVALID_MAP_LIQ_HEIGHT = -2000.f; + static const float INVALID_MAP_LIQ_HEIGHT_MAX = 5000.0f; + + // see following files: + // contrib/extractor/system.cpp + // src/game/Map.cpp + + struct MeshData + { + std::vector<float> solidVerts; + std::vector<int> solidTris; + + std::vector<float> liquidVerts; + std::vector<int> liquidTris; + std::vector<uint8> liquidType; + + // offmesh connection data + std::vector<float> offMeshConnections; // [p0y,p0z,p0x,p1y,p1z,p1x] - per connection + std::vector<float> offMeshConnectionRads; + std::vector<unsigned char> offMeshConnectionDirs; + std::vector<unsigned char> offMeshConnectionsAreas; + std::vector<unsigned short> offMeshConnectionsFlags; + }; + + class TC_MMAPS_COMMON_API TerrainBuilder + { + public: + explicit TerrainBuilder(boost::filesystem::path const& inputDirectory, bool skipLiquid); + + void loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager); + bool loadVMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager); + void loadVMapModel(VMAP::WorldModel const* worldModel, G3D::Vector3 const& position, G3D::Matrix3 const& rotation, float scale, + MeshData& meshData, VMAP::VMapManager* vmapManager); + void loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, std::vector<OffMeshData> const& offMeshConnections); + + bool usesLiquids() const { return !m_skipLiquid; } + + // vert and triangle methods + static void transformVertices(std::vector<G3D::Vector3> const& source, std::vector<float>& dest, + float scale, G3D::Matrix3 const& rotation, G3D::Vector3 const& position); + static void copyIndices(std::vector<VMAP::MeshTriangle> const& source, std::vector<int>& dest, int offset, bool flip); + static void copyIndices(std::vector<int> const& source, std::vector<int>& dest, int offset); + static void cleanVertices(std::vector<float>& verts, std::vector<int>& tris); + private: + /// Loads a portion of a map's terrain + bool loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager, Spot portion); + + /// Sets loop variables for selecting only certain parts of a map's terrain + static void getLoopVars(Spot portion, int& loopStart, int& loopEnd, int& loopInc); + + boost::filesystem::path m_inputDirectory; + + /// Controls whether liquids are loaded + bool m_skipLiquid; + + /// Get the vector coordinate for a specific position + static void getHeightCoord(int index, Grid grid, float xOffset, float yOffset, float* coord, float* v); + + /// Get the triangle's vector indices for a specific position + static void getHeightTriangle(int square, Spot triangle, int* indices, int offset, bool liquid = false); + + /// Determines if the specific position's triangles should be rendered + static bool isHole(int square, uint8 const (&holes)[16][16][8]); + + /// Get the liquid vector coordinate for a specific position + static void getLiquidCoord(int index, int index2, float xOffset, float yOffset, float* coord, float* v); + + /// Get the liquid type for a specific position + static map_liquidHeaderTypeFlags getLiquidType(int square, map_liquidHeaderTypeFlags const (&liquid_type)[16][16]); + }; + + TC_MMAPS_COMMON_API extern std::unique_ptr<VMAP::VMapManager> (*CreateVMapManager)(uint32 mapId); +} + +#endif diff --git a/src/common/mmaps_common/Generator/TileBuilder.cpp b/src/common/mmaps_common/Generator/TileBuilder.cpp new file mode 100644 index 00000000000..31ec90a83da --- /dev/null +++ b/src/common/mmaps_common/Generator/TileBuilder.cpp @@ -0,0 +1,568 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "TileBuilder.h" +#include "IntermediateValues.h" +#include "Log.h" +#include "MMapDefines.h" +#include "Memory.h" +#include "StringFormat.h" +#include "VMapManager.h" +#include <DetourNavMeshBuilder.h> + +namespace +{ + struct Tile + { + Tile() : chf(nullptr), solid(nullptr), cset(nullptr), pmesh(nullptr), dmesh(nullptr) {} + ~Tile() + { + rcFreeCompactHeightfield(chf); + rcFreeContourSet(cset); + rcFreeHeightField(solid); + rcFreePolyMesh(pmesh); + rcFreePolyMeshDetail(dmesh); + } + rcCompactHeightfield* chf; + rcHeightfield* solid; + rcContourSet* cset; + rcPolyMesh* pmesh; + rcPolyMeshDetail* dmesh; + }; +} + +namespace MMAP +{ + struct TileConfig + { + TileConfig(bool bigBaseUnit) + { + // these are WORLD UNIT based metrics + // this are basic unit dimentions + // value have to divide GRID_SIZE(533.3333f) ( aka: 0.5333, 0.2666, 0.3333, 0.1333, etc ) + BASE_UNIT_DIM = bigBaseUnit ? 0.5333333f : 0.2666666f; + + // All are in UNIT metrics! + VERTEX_PER_MAP = int(GRID_SIZE / BASE_UNIT_DIM + 0.5f); + VERTEX_PER_TILE = bigBaseUnit ? 40 : 80; // must divide VERTEX_PER_MAP + TILES_PER_MAP = VERTEX_PER_MAP / VERTEX_PER_TILE; + } + + float BASE_UNIT_DIM; + int VERTEX_PER_MAP; + int VERTEX_PER_TILE; + int TILES_PER_MAP; + }; + + TileBuilder::TileBuilder(boost::filesystem::path const& inputDirectory, boost::filesystem::path const& outputDirectory, + Optional<float> maxWalkableAngle, Optional<float> maxWalkableAngleNotSteep, + bool skipLiquid, bool bigBaseUnit, bool debugOutput, std::vector<OffMeshData> const* offMeshConnections) : + m_outputDirectory(outputDirectory), + m_maxWalkableAngle(maxWalkableAngle), + m_maxWalkableAngleNotSteep(maxWalkableAngleNotSteep), + m_bigBaseUnit(bigBaseUnit), + m_debugOutput(debugOutput), + m_terrainBuilder(inputDirectory, skipLiquid), + m_rcContext(false), + m_offMeshConnections(offMeshConnections) + { + } + + TileBuilder::~TileBuilder() = default; + + /**************************************************************************/ + void TileBuilder::buildTile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh) + { + if (shouldSkipTile(mapID, tileX, tileY)) + { + OnTileDone(); + return; + } + + TC_LOG_INFO("maps.mmapgen", "{} [Map {:04}] Building tile [{:02},{:02}]", GetProgressText(), mapID, tileX, tileY); + + MeshData meshData; + + std::unique_ptr<VMAP::VMapManager> vmapManager = CreateVMapManager(mapID); + + // get heightmap data + m_terrainBuilder.loadMap(mapID, tileX, tileY, meshData, vmapManager.get()); + + // get model data + m_terrainBuilder.loadVMap(mapID, tileX, tileY, meshData, vmapManager.get()); + + // if there is no data, give up now + if (meshData.solidVerts.empty() && meshData.liquidVerts.empty()) + { + OnTileDone(); + return; + } + + // remove unused vertices + TerrainBuilder::cleanVertices(meshData.solidVerts, meshData.solidTris); + TerrainBuilder::cleanVertices(meshData.liquidVerts, meshData.liquidTris); + + if (meshData.liquidVerts.empty() && meshData.solidVerts.empty()) + { + OnTileDone(); + return; + } + + // gather all mesh data for final data check, and bounds calculation + std::vector<float> allVerts(meshData.liquidVerts.size() + meshData.solidVerts.size()); + std::ranges::copy(meshData.liquidVerts, allVerts.begin()); + std::ranges::copy(meshData.solidVerts, allVerts.begin() + std::ssize(meshData.liquidVerts)); + + // get bounds of current tile + float bmin[3], bmax[3]; + getTileBounds(tileX, tileY, allVerts.data(), allVerts.size() / 3, bmin, bmax); + + if (m_offMeshConnections) + m_terrainBuilder.loadOffMeshConnections(mapID, tileX, tileY, meshData, *m_offMeshConnections); + + // build navmesh tile + TileResult tileResult = buildMoveMapTile(mapID, tileX, tileY, meshData, bmin, bmax, navMesh->getParams()); + if (tileResult.data) + saveMoveMapTileToFile(mapID, tileX, tileY, navMesh, tileResult); + + OnTileDone(); + } + + /**************************************************************************/ + TileBuilder::TileResult TileBuilder::buildMoveMapTile(uint32 mapID, uint32 tileX, uint32 tileY, + MeshData& meshData, float (&bmin)[3], float (&bmax)[3], + dtNavMeshParams const* navMeshParams, std::string_view fileNameSuffix) + { + // console output + std::string tileString = Trinity::StringFormat("[Map {:04}] [{:02},{:02}]:", mapID, tileX, tileY); + TC_LOG_INFO("maps.mmapgen", "{} Building movemap tile...", tileString); + + TileResult tileResult; + + IntermediateValues iv; + + float* tVerts = meshData.solidVerts.data(); + int tVertCount = meshData.solidVerts.size() / 3; + int* tTris = meshData.solidTris.data(); + int tTriCount = meshData.solidTris.size() / 3; + + float* lVerts = meshData.liquidVerts.data(); + int lVertCount = meshData.liquidVerts.size() / 3; + int* lTris = meshData.liquidTris.data(); + int lTriCount = meshData.liquidTris.size() / 3; + uint8* lTriFlags = meshData.liquidType.data(); + + const TileConfig tileConfig = TileConfig(m_bigBaseUnit); + int TILES_PER_MAP = tileConfig.TILES_PER_MAP; + float BASE_UNIT_DIM = tileConfig.BASE_UNIT_DIM; + rcConfig config = GetMapSpecificConfig(mapID, bmin, bmax, tileConfig); + + // this sets the dimensions of the heightfield - should maybe happen before border padding + rcCalcGridSize(config.bmin, config.bmax, config.cs, &config.width, &config.height); + + // allocate subregions : tiles + std::unique_ptr<Tile[]> tiles = std::make_unique<Tile[]>(TILES_PER_MAP * TILES_PER_MAP); + + // Initialize per tile config. + rcConfig tileCfg = config; + tileCfg.width = config.tileSize + config.borderSize * 2; + tileCfg.height = config.tileSize + config.borderSize * 2; + + // merge per tile poly and detail meshes + std::unique_ptr<rcPolyMesh*[]> pmmerge = std::make_unique<rcPolyMesh*[]>(TILES_PER_MAP * TILES_PER_MAP); + std::unique_ptr<rcPolyMeshDetail*[]> dmmerge = std::make_unique<rcPolyMeshDetail*[]>(TILES_PER_MAP * TILES_PER_MAP); + int nmerge = 0; + // build all tiles + for (int y = 0; y < TILES_PER_MAP; ++y) + { + for (int x = 0; x < TILES_PER_MAP; ++x) + { + Tile& tile = tiles[x + y * TILES_PER_MAP]; + + // Calculate the per tile bounding box. + tileCfg.bmin[0] = config.bmin[0] + x * float(config.tileSize * config.cs); + tileCfg.bmin[2] = config.bmin[2] + y * float(config.tileSize * config.cs); + tileCfg.bmax[0] = config.bmin[0] + (x + 1) * float(config.tileSize * config.cs); + tileCfg.bmax[2] = config.bmin[2] + (y + 1) * float(config.tileSize * config.cs); + + tileCfg.bmin[0] -= tileCfg.borderSize * tileCfg.cs; + tileCfg.bmin[2] -= tileCfg.borderSize * tileCfg.cs; + tileCfg.bmax[0] += tileCfg.borderSize * tileCfg.cs; + tileCfg.bmax[2] += tileCfg.borderSize * tileCfg.cs; + + // build heightfield + tile.solid = rcAllocHeightfield(); + if (!tile.solid || !rcCreateHeightfield(&m_rcContext, *tile.solid, tileCfg.width, tileCfg.height, tileCfg.bmin, tileCfg.bmax, tileCfg.cs, tileCfg.ch)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building heightfield!", tileString); + continue; + } + + // mark all walkable tiles, both liquids and solids + + /* we want to have triangles with slope less than walkableSlopeAngleNotSteep (<= 55) to have NAV_AREA_GROUND + * and with slope between walkableSlopeAngleNotSteep and walkableSlopeAngle (55 < .. <= 70) to have NAV_AREA_GROUND_STEEP. + * we achieve this using recast API: memset everything to NAV_AREA_GROUND_STEEP, call rcClearUnwalkableTriangles with 70 so + * any area above that will get RC_NULL_AREA (unwalkable), then call rcMarkWalkableTriangles with 55 to set NAV_AREA_GROUND + * on anything below 55 . Players and idle Creatures can use NAV_AREA_GROUND, while Creatures in combat can use NAV_AREA_GROUND_STEEP. + */ + std::unique_ptr<unsigned char[]> triFlags = std::make_unique<unsigned char[]>(tTriCount); + memset(triFlags.get(), NAV_AREA_GROUND_STEEP, tTriCount * sizeof(unsigned char)); + rcClearUnwalkableTriangles(&m_rcContext, tileCfg.walkableSlopeAngle, tVerts, tVertCount, tTris, tTriCount, triFlags.get()); + rcMarkWalkableTriangles(&m_rcContext, tileCfg.walkableSlopeAngleNotSteep, tVerts, tVertCount, tTris, tTriCount, triFlags.get(), NAV_AREA_GROUND); + rcRasterizeTriangles(&m_rcContext, tVerts, tVertCount, tTris, triFlags.get(), tTriCount, *tile.solid, config.walkableClimb); + + rcFilterLowHangingWalkableObstacles(&m_rcContext, config.walkableClimb, *tile.solid); + rcFilterLedgeSpans(&m_rcContext, tileCfg.walkableHeight, tileCfg.walkableClimb, *tile.solid); + rcFilterWalkableLowHeightSpans(&m_rcContext, tileCfg.walkableHeight, *tile.solid); + + // add liquid triangles + rcRasterizeTriangles(&m_rcContext, lVerts, lVertCount, lTris, lTriFlags, lTriCount, *tile.solid, config.walkableClimb); + + // compact heightfield spans + tile.chf = rcAllocCompactHeightfield(); + if (!tile.chf || !rcBuildCompactHeightfield(&m_rcContext, tileCfg.walkableHeight, tileCfg.walkableClimb, *tile.solid, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed compacting heightfield!", tileString); + continue; + } + + // build polymesh intermediates + if (!rcErodeWalkableArea(&m_rcContext, config.walkableRadius, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed eroding area!", tileString); + continue; + } + + if (!rcMedianFilterWalkableArea(&m_rcContext, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed filtering area!", tileString); + continue; + } + + if (!rcBuildDistanceField(&m_rcContext, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building distance field!", tileString); + continue; + } + + if (!rcBuildRegions(&m_rcContext, *tile.chf, tileCfg.borderSize, tileCfg.minRegionArea, tileCfg.mergeRegionArea)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building regions!", tileString); + continue; + } + + tile.cset = rcAllocContourSet(); + if (!tile.cset || !rcBuildContours(&m_rcContext, *tile.chf, tileCfg.maxSimplificationError, tileCfg.maxEdgeLen, *tile.cset)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building contours!", tileString); + continue; + } + + // build polymesh + tile.pmesh = rcAllocPolyMesh(); + if (!tile.pmesh || !rcBuildPolyMesh(&m_rcContext, *tile.cset, tileCfg.maxVertsPerPoly, *tile.pmesh)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building polymesh!", tileString); + continue; + } + + tile.dmesh = rcAllocPolyMeshDetail(); + if (!tile.dmesh || !rcBuildPolyMeshDetail(&m_rcContext, *tile.pmesh, *tile.chf, tileCfg.detailSampleDist, tileCfg.detailSampleMaxError, *tile.dmesh)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building polymesh detail!", tileString); + continue; + } + + // free those up + // we may want to keep them in the future for debug + // but right now, we don't have the code to merge them + rcFreeHeightField(tile.solid); + tile.solid = nullptr; + rcFreeCompactHeightfield(tile.chf); + tile.chf = nullptr; + rcFreeContourSet(tile.cset); + tile.cset = nullptr; + + pmmerge[nmerge] = tile.pmesh; + dmmerge[nmerge] = tile.dmesh; + nmerge++; + } + } + + iv.polyMesh = rcAllocPolyMesh(); + if (!iv.polyMesh) + { + TC_LOG_ERROR("maps.mmapgen", "{} alloc iv.polyMesh FAILED!", tileString); + return tileResult; + } + rcMergePolyMeshes(&m_rcContext, pmmerge.get(), nmerge, *iv.polyMesh); + + iv.polyMeshDetail = rcAllocPolyMeshDetail(); + if (!iv.polyMeshDetail) + { + TC_LOG_ERROR("maps.mmapgen", "{} alloc m_dmesh FAILED!", tileString); + return tileResult; + } + rcMergePolyMeshDetails(&m_rcContext, dmmerge.get(), nmerge, *iv.polyMeshDetail); + + // free things up + pmmerge = nullptr; + dmmerge = nullptr; + tiles = nullptr; + + // set polygons as walkable + // TODO: special flags for DYNAMIC polygons, ie surfaces that can be turned on and off + for (int i = 0; i < iv.polyMesh->npolys; ++i) + { + if (uint8 area = iv.polyMesh->areas[i] & NAV_AREA_ALL_MASK) + { + if (area >= NAV_AREA_MIN_VALUE) + iv.polyMesh->flags[i] = 1 << (NAV_AREA_MAX_VALUE - area); + else + iv.polyMesh->flags[i] = NAV_GROUND; // TODO: these will be dynamic in future + } + } + + // setup mesh parameters + dtNavMeshCreateParams params = {}; + params.verts = iv.polyMesh->verts; + params.vertCount = iv.polyMesh->nverts; + params.polys = iv.polyMesh->polys; + params.polyAreas = iv.polyMesh->areas; + params.polyFlags = iv.polyMesh->flags; + params.polyCount = iv.polyMesh->npolys; + params.nvp = iv.polyMesh->nvp; + params.detailMeshes = iv.polyMeshDetail->meshes; + params.detailVerts = iv.polyMeshDetail->verts; + params.detailVertsCount = iv.polyMeshDetail->nverts; + params.detailTris = iv.polyMeshDetail->tris; + params.detailTriCount = iv.polyMeshDetail->ntris; + + params.offMeshConVerts = meshData.offMeshConnections.data(); + params.offMeshConCount = meshData.offMeshConnections.size() / 6; + params.offMeshConRad = meshData.offMeshConnectionRads.data(); + params.offMeshConDir = meshData.offMeshConnectionDirs.data(); + params.offMeshConAreas = meshData.offMeshConnectionsAreas.data(); + params.offMeshConFlags = meshData.offMeshConnectionsFlags.data(); + + params.walkableHeight = BASE_UNIT_DIM * config.walkableHeight; // agent height + params.walkableRadius = BASE_UNIT_DIM * config.walkableRadius; // agent radius + params.walkableClimb = BASE_UNIT_DIM * config.walkableClimb; // keep less that walkableHeight (aka agent height)! + params.tileX = (((bmin[0] + bmax[0]) / 2) - navMeshParams->orig[0]) / GRID_SIZE; + params.tileY = (((bmin[2] + bmax[2]) / 2) - navMeshParams->orig[2]) / GRID_SIZE; + rcVcopy(params.bmin, bmin); + rcVcopy(params.bmax, bmax); + params.cs = config.cs; + params.ch = config.ch; + params.tileLayer = 0; + params.buildBvTree = true; + + // will hold final navmesh + unsigned char* navData = nullptr; + + auto debugOutputWriter = Trinity::make_unique_ptr_with_deleter(m_debugOutput ? &iv : nullptr, + [borderSize = static_cast<unsigned short>(config.borderSize), + outputDir = &m_outputDirectory, fileNameSuffix, + mapID, tileX, tileY, &meshData](IntermediateValues* intermediate) + { + // restore padding so that the debug visualization is correct + for (std::ptrdiff_t i = 0; i < intermediate->polyMesh->nverts; ++i) + { + unsigned short* v = &intermediate->polyMesh->verts[i * 3]; + v[0] += borderSize; + v[2] += borderSize; + } + + intermediate->generateObjFile(*outputDir, fileNameSuffix, mapID, tileX, tileY, meshData); + intermediate->writeIV(*outputDir, fileNameSuffix, mapID, tileX, tileY); + }); + + // these values are checked within dtCreateNavMeshData - handle them here + // so we have a clear error message + if (params.nvp > DT_VERTS_PER_POLYGON) + { + TC_LOG_ERROR("maps.mmapgen", "{} Invalid verts-per-polygon value!", tileString); + return tileResult; + } + + if (params.vertCount >= 0xffff) + { + TC_LOG_ERROR("maps.mmapgen", "{} Too many vertices!", tileString); + return tileResult; + } + + if (!params.vertCount || !params.verts) + { + // occurs mostly when adjacent tiles have models + // loaded but those models don't span into this tile + + // message is an annoyance + //TC_LOG_ERROR("maps.mmapgen", "{} No vertices to build tile!", tileString); + return tileResult; + } + + if (!params.polyCount || !params.polys) + { + // we have flat tiles with no actual geometry - don't build those, its useless + // keep in mind that we do output those into debug info + TC_LOG_ERROR("maps.mmapgen", "{} No polygons to build on tile!", tileString); + return tileResult; + } + + if (!params.detailMeshes || !params.detailVerts || !params.detailTris) + { + TC_LOG_ERROR("maps.mmapgen", "{} No detail mesh to build tile!", tileString); + return tileResult; + } + + TC_LOG_DEBUG("maps.mmapgen", "{} Building navmesh tile...", tileString); + if (!dtCreateNavMeshData(¶ms, &navData, &tileResult.size)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building navmesh tile!", tileString); + return tileResult; + } + + tileResult.data.reset(navData); + return tileResult; + } + + void TileBuilder::saveMoveMapTileToFile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh, + TileResult const& tileResult, std::string_view fileNameSuffix) + { + dtTileRef tileRef = 0; + auto navMeshTile = Trinity::make_unique_ptr_with_deleter<dtTileRef*>(nullptr, [navMesh](dtTileRef const* ref) + { + navMesh->removeTile(*ref, nullptr, nullptr); + }); + + if (navMesh) + { + TC_LOG_DEBUG("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: Adding tile to navmesh...", mapID, tileX, tileY); + // DT_TILE_FREE_DATA tells detour to unallocate memory when the tile + // is removed via removeTile() + dtStatus dtResult = navMesh->addTile(tileResult.data.get(), tileResult.size, 0, 0, &tileRef); + if (!tileRef || !dtStatusSucceed(dtResult)) + { + TC_LOG_ERROR("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: Failed adding tile to navmesh!", mapID, tileX, tileY); + return; + } + + navMeshTile.reset(&tileRef); + } + + // file output + std::string fileName = Trinity::StringFormat("{}/mmaps/{:04}_{:02}_{:02}{}.mmtile", m_outputDirectory.generic_string(), mapID, tileX, tileY, fileNameSuffix); + auto file = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(fileName.c_str(), "wb")); + if (!file) + { + TC_LOG_ERROR("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: {}: Failed to open {} for writing!", mapID, tileX, tileY, strerror(errno), fileName); + return; + } + + TC_LOG_DEBUG("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: Writing to file...", mapID, tileX, tileY); + + // write header + MmapTileHeader header; + header.usesLiquids = m_terrainBuilder.usesLiquids(); + header.size = uint32(tileResult.size); + fwrite(&header, sizeof(MmapTileHeader), 1, file.get()); + + // write data + fwrite(tileResult.data.get(), sizeof(unsigned char), tileResult.size, file.get()); + } + + /**************************************************************************/ + void TileBuilder::getTileBounds(uint32 tileX, uint32 tileY, float const* verts, std::size_t vertCount, float* bmin, float* bmax) + { + // this is for elevation + if (verts && vertCount) + rcCalcBounds(verts, int(vertCount), bmin, bmax); + else + { + bmin[1] = FLT_MIN; + bmax[1] = FLT_MAX; + } + + // this is for width and depth + bmax[0] = (32 - int(tileY)) * GRID_SIZE; + bmax[2] = (32 - int(tileX)) * GRID_SIZE; + bmin[0] = bmax[0] - GRID_SIZE; + bmin[2] = bmax[2] - GRID_SIZE; + } + + /**************************************************************************/ + bool TileBuilder::shouldSkipTile(uint32 /*mapID*/, uint32 /*tileX*/, uint32 /*tileY*/) const + { + if (m_debugOutput) + return false; + + return true; + } + + rcConfig TileBuilder::GetMapSpecificConfig(uint32 mapID, float const (&bmin)[3], float const (&bmax)[3], TileConfig const& tileConfig) const + { + rcConfig config { }; + + rcVcopy(config.bmin, bmin); + rcVcopy(config.bmax, bmax); + + config.maxVertsPerPoly = DT_VERTS_PER_POLYGON; + config.cs = tileConfig.BASE_UNIT_DIM; + config.ch = tileConfig.BASE_UNIT_DIM; + // Keeping these 2 slope angles the same reduces a lot the number of polys. + // 55 should be the minimum, maybe 70 is ok (keep in mind blink uses mmaps), 85 is too much for players + config.walkableSlopeAngle = m_maxWalkableAngle.value_or(55.0f); + config.walkableSlopeAngleNotSteep = m_maxWalkableAngleNotSteep.value_or(55.0f); + config.tileSize = tileConfig.VERTEX_PER_TILE; + config.walkableRadius = m_bigBaseUnit ? 1 : 2; + config.borderSize = config.walkableRadius + 3; + config.maxEdgeLen = tileConfig.VERTEX_PER_TILE + 1; // anything bigger than tileSize + config.walkableHeight = m_bigBaseUnit ? 3 : 6; + // a value >= 3|6 allows npcs to walk over some fences + // a value >= 4|8 allows npcs to walk over all fences + config.walkableClimb = m_bigBaseUnit ? 3 : 6; + config.minRegionArea = rcSqr(60); + config.mergeRegionArea = rcSqr(50); + config.maxSimplificationError = 1.8f; // eliminates most jagged edges (tiny polygons) + config.detailSampleDist = config.cs * 16; + config.detailSampleMaxError = config.ch * 1; + + switch (mapID) + { + // Blade's Edge Arena + case 562: + // This allows to walk on the ropes to the pillars + config.walkableRadius = 0; + break; + // Blackfathom Deeps + case 48: + // Reduce the chance to have underground levels + config.ch *= 2; + break; + default: + break; + } + + return config; + } + + std::string TileBuilder::GetProgressText() const + { + return ""; + } +} diff --git a/src/common/mmaps_common/Generator/TileBuilder.h b/src/common/mmaps_common/Generator/TileBuilder.h new file mode 100644 index 00000000000..f3492e51a2e --- /dev/null +++ b/src/common/mmaps_common/Generator/TileBuilder.h @@ -0,0 +1,100 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_TILE_BUILDER_H +#define TRINITYCORE_TILE_BUILDER_H + +#include "Common.h" +#include "Memory.h" +#include "StringFormat.h" +#include "TerrainBuilder.h" +#include <DetourNavMesh.h> +#include <Recast.h> + +namespace MMAP +{ +struct TileConfig; + +using detour_unique_ptr = std::unique_ptr<unsigned char, decltype(Trinity::unique_ptr_deleter<unsigned char*, &::dtFree>())>; + +class TC_MMAPS_COMMON_API TileBuilder +{ +public: + TileBuilder(boost::filesystem::path const& inputDirectory, + boost::filesystem::path const& outputDirectory, + Optional<float> maxWalkableAngle, + Optional<float> maxWalkableAngleNotSteep, + bool skipLiquid, + bool bigBaseUnit, + bool debugOutput, + std::vector<OffMeshData> const* offMeshConnections); + + TileBuilder(TileBuilder const&) = delete; + TileBuilder(TileBuilder&&) = delete; + + TileBuilder& operator=(TileBuilder const&) = delete; + TileBuilder& operator=(TileBuilder&&) = delete; + + virtual ~TileBuilder(); + + void buildTile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh); + + // move map building + struct TileResult + { + detour_unique_ptr data; + int size = 0; + }; + TileResult buildMoveMapTile(uint32 mapID, + uint32 tileX, + uint32 tileY, + MeshData& meshData, + float (&bmin)[3], + float (&bmax)[3], + dtNavMeshParams const* navMeshParams, + std::string_view fileNameSuffix = ""sv); + + void saveMoveMapTileToFile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh, + TileResult const& tileResult, std::string_view fileNameSuffix = ""sv); + + virtual bool shouldSkipTile(uint32 mapID, uint32 tileX, uint32 tileY) const; + + static void getTileBounds(uint32 tileX, uint32 tileY, + float const* verts, std::size_t vertCount, + float* bmin, float* bmax); + + rcConfig GetMapSpecificConfig(uint32 mapID, float const (&bmin)[3], float const (&bmax)[3], TileConfig const& tileConfig) const; + + virtual std::string GetProgressText() const; + + virtual void OnTileDone() { } + +protected: + boost::filesystem::path m_outputDirectory; + Optional<float> m_maxWalkableAngle; + Optional<float> m_maxWalkableAngleNotSteep; + bool m_bigBaseUnit; + bool m_debugOutput; + + TerrainBuilder m_terrainBuilder; + // build performance - not really used for now + rcContext m_rcContext; + std::vector<OffMeshData> const* m_offMeshConnections; +}; +} + +#endif // TRINITYCORE_TILE_BUILDER_H diff --git a/src/common/Collision/Maps/MMapDefines.h b/src/common/mmaps_common/MMapDefines.h index 522112ebe62..522112ebe62 100644 --- a/src/common/Collision/Maps/MMapDefines.h +++ b/src/common/mmaps_common/MMapDefines.h diff --git a/src/common/Collision/Management/MMapManager.cpp b/src/common/mmaps_common/Management/MMapManager.cpp index d0862a49156..a45c1ee5fbb 100644 --- a/src/common/Collision/Management/MMapManager.cpp +++ b/src/common/mmaps_common/Management/MMapManager.cpp @@ -20,6 +20,7 @@ #include "Hash.h" #include "Log.h" #include "MMapDefines.h" +#include "MapUtils.h" #include "Memory.h" #include <algorithm> @@ -28,22 +29,57 @@ namespace MMAP constexpr char MAP_FILE_NAME_FORMAT[] = "{}mmaps/{:04}.mmap"; constexpr char TILE_FILE_NAME_FORMAT[] = "{}mmaps/{:04}_{:02}_{:02}.mmtile"; - using NavMeshPtr = std::unique_ptr<dtNavMesh, decltype(Trinity::unique_ptr_deleter<dtNavMesh*, &::dtFreeNavMesh>())>; - using NavMeshQueryPtr = std::unique_ptr<dtNavMeshQuery, decltype(Trinity::unique_ptr_deleter<dtNavMeshQuery*, &::dtFreeNavMeshQuery>())>; + static thread_local bool thread_safe_environment = false; - typedef std::unordered_map<std::pair<uint32, uint32>, NavMeshQueryPtr> NavMeshQuerySet; - typedef std::unordered_map<uint32, dtTileRef> MMapTileSet; + using NavMeshQuerySet = std::unordered_map<std::pair<uint32, uint32>, dtNavMeshQuery>; + using MMapTileSet = std::unordered_map<uint32, dtTileRef>; + + struct MMapMapData + { + dtNavMesh navMesh; + MMapTileSet loadedTileRefs; // maps [map grid coords] to [dtTile] + }; + + using MeshDataMap = std::unordered_map<uint32, MMapMapData>; // dummy struct to hold map's mmap data struct MMapData { - explicit MMapData(NavMeshPtr&& mesh) : navMesh(std::move(mesh)) { } + MeshDataMap meshData; // we have to use single dtNavMeshQuery for every instance, since those are not thread safe NavMeshQuerySet navMeshQueries; // instanceId to query - NavMeshPtr navMesh; - MMapTileSet loadedTileRefs; // maps [map grid coords] to [dtTile] + static uint32 GetInstanceIdForMeshLookup(uint32 mapId, uint32 instanceId) + { + switch (mapId) + { + case 0: case 1: case 571: case 603: case 607: case 609: case 616: case 628: case 631: case 644: case 649: case 720: + case 732: case 754: case 755: case 861: case 938: case 940: case 962: case 967: case 1064: case 1076: case 1098: + case 1122: case 1126: case 1182: case 1205: case 1220: case 1265: case 1492: case 1523: case 1530: case 1579: case 1676: + case 1704: case 1705: case 1706: case 1707: case 1734: case 1756: case 1943: case 2076: case 2118: case 2160: case 2161: + case 2187: case 2212: case 2235: case 2237: case 2264: case 2450: case 2512: case 2586: case 2601: case 2654: case 2657: + case 2660: case 2669: case 2819: case 2828: + return instanceId; + default: + break; + } + + // for maps that won't have dynamic mesh, return 0 to reuse the same mesh across all instances + return 0; + } + + std::pair<MeshDataMap::iterator, bool> GetMeshData(uint32 mapId, uint32 instanceId) + { + // for maps that won't have dynamic mesh, return 0 to reuse the same mesh across all instances + return meshData.try_emplace(GetInstanceIdForMeshLookup(mapId, instanceId)); + } + + MeshDataMap::iterator FindMeshData(uint32 mapId, uint32 instanceId) + { + // for maps that won't have dynamic mesh, return 0 to reuse the same mesh across all instances + return meshData.find(GetInstanceIdForMeshLookup(mapId, instanceId)); + } }; // ######################## MMapManager ######################## @@ -61,12 +97,13 @@ namespace MMAP // the caller must pass the list of all mapIds that will be used in the MMapManager lifetime for (auto const& [mapId, childMapIds] : mapData) { - loadedMMaps.insert(MMapDataSet::value_type(mapId, nullptr)); + loadedMMaps[mapId].reset(new MMapData()); for (uint32 childMapId : childMapIds) parentMapData[childMapId] = mapId; } - thread_safe_environment = false; + // mark the loading main thread as safe + thread_safe_environment = true; } MMapDataSet::const_iterator MMapManager::GetMMapData(uint32 mapId) const @@ -79,40 +116,52 @@ namespace MMAP return itr; } - LoadResult MMapManager::loadMapData(std::string_view basePath, uint32 mapId) + bool MMapManager::isRebuildingTilesEnabledOnMap(uint32 mapId) + { + return MMapData::GetInstanceIdForMeshLookup(mapId, 1) != 0; + } + + LoadResult MMapManager::loadMapData(std::string_view basePath, uint32 mapId, uint32 instanceId) { // we already have this map loaded? - MMapDataSet::iterator itr = loadedMMaps.find(mapId); - if (itr != loadedMMaps.end()) + MMapDataSet::iterator itr; + if (thread_safe_environment) { - if (itr->second) - return LoadResult::AlreadyLoaded; + bool needsLoading; + std::tie(itr, needsLoading) = loadedMMaps.try_emplace(mapId); + if (needsLoading) + itr->second.reset(new MMapData()); } else { - if (thread_safe_environment) - itr = loadedMMaps.insert(MMapDataSet::value_type(mapId, nullptr)).first; - else + itr = loadedMMaps.find(mapId); + if (itr == loadedMMaps.end()) ABORT_MSG("Invalid mapId %u passed to MMapManager after startup in thread unsafe environment", mapId); } + auto [meshItr, needsLoading] = itr->second->GetMeshData(mapId, instanceId); + if (!needsLoading) + return LoadResult::AlreadyLoaded; + + auto loadGuard = Trinity::make_unique_ptr_with_deleter(&meshItr, [&](MeshDataMap::iterator* m) + { + itr->second->meshData.erase(*m); + }); + // load and init dtNavMesh - read parameters from file dtNavMeshParams params; if (LoadResult paramsResult = parseNavMeshParamsFile(basePath, mapId, ¶ms); paramsResult != LoadResult::Success) return paramsResult; - NavMeshPtr mesh(dtAllocNavMesh()); - ASSERT(mesh); - if (dtStatusFailed(mesh->init(¶ms))) + if (dtStatusFailed(meshItr->second.navMesh.init(¶ms))) { TC_LOG_ERROR("maps", "MMAP:loadMapData: Failed to initialize dtNavMesh for mmap {:04}", mapId); return LoadResult::LibraryError; } TC_LOG_DEBUG("maps", "MMAP:loadMapData: Loaded {:04}.mmap", mapId); + (void)loadGuard.release(); - // store inside our map list - itr->second.reset(new MMapData(std::move(mesh))); return LoadResult::Success; } @@ -168,10 +217,10 @@ namespace MMAP return uint32(x << 16 | y); } - LoadResult MMapManager::loadMap(std::string_view basePath, uint32 mapId, int32 x, int32 y) + LoadResult MMapManager::loadMap(std::string_view basePath, uint32 mapId, uint32 instanceId, int32 x, int32 y) { // make sure the mmap is loaded and ready to load tiles - switch (LoadResult mapResult = loadMapData(basePath, mapId)) + switch (LoadResult mapResult = loadMapData(basePath, mapId, instanceId)) { case LoadResult::Success: case LoadResult::AlreadyLoaded: @@ -182,11 +231,11 @@ namespace MMAP // get this mmap data MMapData* mmap = loadedMMaps[mapId].get(); - ASSERT(mmap->navMesh); + MMapMapData& meshData = mmap->GetMeshData(mapId, instanceId).first->second; // check if we already have this tile loaded uint32 packedGridPos = packTileID(x, y); - if (mmap->loadedTileRefs.contains(packedGridPos)) + if (meshData.loadedTileRefs.contains(packedGridPos)) return LoadResult::AlreadyLoaded; // load this tile :: mmaps/MMMM_XX_YY.mmtile @@ -253,9 +302,9 @@ namespace MMAP dtTileRef tileRef = 0; // memory allocated for data is now managed by detour, and will be deallocated when the tile is removed - if (dtStatusSucceed(mmap->navMesh->addTile(static_cast<unsigned char*>(data.release()), fileHeader.size, DT_TILE_FREE_DATA, 0, &tileRef))) + if (dtStatusSucceed(meshData.navMesh.addTile(static_cast<unsigned char*>(data.release()), fileHeader.size, DT_TILE_FREE_DATA, 0, &tileRef))) { - mmap->loadedTileRefs.insert(std::pair<uint32, dtTileRef>(packedGridPos, tileRef)); + meshData.loadedTileRefs[packedGridPos] = tileRef; ++loadedTiles; TC_LOG_DEBUG("maps", "MMAP:loadMap: Loaded mmtile {:04}[{:02}, {:02}] into {:04}[{:02}, {:02}]", mapId, x, y, mapId, header->x, header->y); return LoadResult::Success; @@ -269,7 +318,7 @@ namespace MMAP bool MMapManager::loadMapInstance(std::string_view basePath, uint32 meshMapId, uint32 instanceMapId, uint32 instanceId) { - switch (loadMapData(basePath, meshMapId)) + switch (loadMapData(basePath, meshMapId, instanceId)) { case LoadResult::Success: case LoadResult::AlreadyLoaded: @@ -279,14 +328,17 @@ namespace MMAP } MMapData* mmap = loadedMMaps[meshMapId].get(); - auto [queryItr, inserted] = mmap->navMeshQueries.try_emplace({ instanceMapId, instanceId }, nullptr); + auto [queryItr, inserted] = mmap->navMeshQueries.try_emplace({ instanceMapId, instanceId }); if (!inserted) return true; + auto loadGuard = Trinity::make_unique_ptr_with_deleter(&queryItr, [&](NavMeshQuerySet::iterator* m) + { + mmap->navMeshQueries.erase(*m); + }); + // allocate mesh query - NavMeshQueryPtr query(dtAllocNavMeshQuery()); - ASSERT(query); - if (dtStatusFailed(query->init(mmap->navMesh.get(), 1024))) + if (dtStatusFailed(queryItr->second.init(&mmap->GetMeshData(meshMapId, instanceId).first->second.navMesh, 1024))) { mmap->navMeshQueries.erase(queryItr); TC_LOG_ERROR("maps", "MMAP:GetNavMeshQuery: Failed to initialize dtNavMeshQuery for mapId {:04} instanceId {}", instanceMapId, instanceId); @@ -294,11 +346,11 @@ namespace MMAP } TC_LOG_DEBUG("maps", "MMAP:GetNavMeshQuery: created dtNavMeshQuery for mapId {:04} instanceId {}", instanceMapId, instanceId); - queryItr->second = std::move(query); + (void)loadGuard.release(); return true; } - bool MMapManager::unloadMap(uint32 mapId, int32 x, int32 y) + void MMapManager::unloadMap(uint32 mapId, int32 x, int32 y) { // check if we have this map loaded MMapDataSet::const_iterator itr = GetMMapData(mapId); @@ -306,72 +358,69 @@ namespace MMAP { // file may not exist, therefore not loaded TC_LOG_DEBUG("maps", "MMAP:unloadMap: Asked to unload not loaded navmesh map. {:04}_{:02}_{:02}.mmtile", mapId, x, y); - return false; + return; } MMapData* mmap = itr->second.get(); - - // check if we have this tile loaded uint32 packedGridPos = packTileID(x, y); - auto tileRef = mmap->loadedTileRefs.extract(packedGridPos); - if (!tileRef) + for (auto& [instanceId, meshData] : mmap->meshData) { - // file may not exist, therefore not loaded - TC_LOG_DEBUG("maps", "MMAP:unloadMap: Asked to unload not loaded navmesh tile. {:04}{:02}{:02}.mmtile", mapId, x, y); - return false; - } + // check if we have this tile loaded + auto tileRef = meshData.loadedTileRefs.extract(packedGridPos); + if (!tileRef) + continue; - // unload, and mark as non loaded - if (dtStatusFailed(mmap->navMesh->removeTile(tileRef.mapped(), nullptr, nullptr))) - { - // this is technically a memory leak - // if the grid is later reloaded, dtNavMesh::addTile will return error but no extra memory is used - // we cannot recover from this error - assert out - TC_LOG_ERROR("maps", "MMAP:unloadMap: Could not unload {:04}_{:02}_{:02}.mmtile from navmesh", mapId, x, y); - ABORT(); - } - else - { - --loadedTiles; - TC_LOG_DEBUG("maps", "MMAP:unloadMap: Unloaded mmtile {:04}[{:02}, {:02}] from {:03}", mapId, x, y, mapId); - return true; + // unload, and mark as non loaded + if (dtStatusFailed(meshData.navMesh.removeTile(tileRef.mapped(), nullptr, nullptr))) + { + // this is technically a memory leak + // if the grid is later reloaded, dtNavMesh::addTile will return error but no extra memory is used + // we cannot recover from this error - assert out + TC_LOG_ERROR("maps", "MMAP:unloadMap: Could not unload {:04}_{:02}_{:02}.mmtile from navmesh", mapId, x, y); + ABORT(); + } + else + { + --loadedTiles; + TC_LOG_DEBUG("maps", "MMAP:unloadMap: Unloaded mmtile {:04}[{:02}, {:02}] from {:03}", mapId, x, y, mapId); + } } - - return false; } - bool MMapManager::unloadMap(uint32 mapId) + void MMapManager::unloadMap(uint32 mapId) { MMapDataSet::iterator itr = loadedMMaps.find(mapId); if (itr == loadedMMaps.end() || !itr->second) { // file may not exist, therefore not loaded TC_LOG_DEBUG("maps", "MMAP:unloadMap: Asked to unload not loaded navmesh map {:04}", mapId); - return false; + return; } - // unload all tiles from given map - MMapData* mmap = itr->second.get(); - for (auto const& [tileId, tileRef] : mmap->loadedTileRefs) + if (MMapData::GetInstanceIdForMeshLookup(mapId, std::numeric_limits<uint32>::max()) == 0) { - uint32 x = (tileId >> 16); - uint32 y = (tileId & 0x0000FFFF); - if (dtStatusFailed(mmap->navMesh->removeTile(tileRef, nullptr, nullptr))) - TC_LOG_ERROR("maps", "MMAP:unloadMap: Could not unload {:04}_{:02}_{:02}.mmtile from navmesh", mapId, x, y); - else + // unload all tiles from given map + MMapMapData& mesh = itr->second->meshData[0]; + for (auto const& [tileId, tileRef] : mesh.loadedTileRefs) { - --loadedTiles; - TC_LOG_DEBUG("maps", "MMAP:unloadMap: Unloaded mmtile {:04}[{:02}, {:02}] from {:04}", mapId, x, y, mapId); + uint32 x = (tileId >> 16); + uint32 y = (tileId & 0x0000FFFF); + if (dtStatusFailed(mesh.navMesh.removeTile(tileRef, nullptr, nullptr))) + TC_LOG_ERROR("maps", "MMAP:unloadMap: Could not unload {:04}_{:02}_{:02}.mmtile from navmesh", mapId, x, y); + else + { + --loadedTiles; + TC_LOG_DEBUG("maps", "MMAP:unloadMap: Unloaded mmtile {:04}[{:02}, {:02}] from {:04}", mapId, x, y, mapId); + } } } + else // require all tiles to be already unloaded + ASSERT(std::ranges::all_of(itr->second->meshData, [](MMapMapData const& mesh) { return mesh.loadedTileRefs.empty(); }, Trinity::Containers::MapValue)); - itr->second = nullptr; TC_LOG_DEBUG("maps", "MMAP:unloadMap: Unloaded {:04}.mmap", mapId); - - return true; } - bool MMapManager::unloadMapInstance(uint32 meshMapId, uint32 instanceMapId, uint32 instanceId) + void MMapManager::unloadMapInstance(uint32 meshMapId, uint32 instanceMapId, uint32 instanceId) { // check if we have this map loaded MMapDataSet::const_iterator itr = GetMMapData(meshMapId); @@ -379,29 +428,48 @@ namespace MMAP { // file may not exist, therefore not loaded TC_LOG_DEBUG("maps", "MMAP:unloadMapInstance: Asked to unload not loaded navmesh map {:04}", meshMapId); - return false; + return; } MMapData* mmap = itr->second.get(); std::size_t erased = mmap->navMeshQueries.erase({ instanceMapId, instanceId }); if (!erased) - { TC_LOG_DEBUG("maps", "MMAP:unloadMapInstance: Asked to unload not loaded dtNavMeshQuery mapId {:04} instanceId {}", instanceMapId, instanceId); - return false; + + MeshDataMap::iterator meshItr = mmap->FindMeshData(meshMapId, instanceId); + if (meshItr != mmap->meshData.end()) + { + // unload all tiles from given map + for (auto const& [tileId, tileRef] : meshItr->second.loadedTileRefs) + { + uint32 x = (tileId >> 16); + uint32 y = (tileId & 0x0000FFFF); + if (dtStatusFailed(meshItr->second.navMesh.removeTile(tileRef, nullptr, nullptr))) + TC_LOG_ERROR("maps", "MMAP:unloadMap: Could not unload {:04}_{:02}_{:02}.mmtile from navmesh", meshMapId, x, y); + else + { + --loadedTiles; + TC_LOG_DEBUG("maps", "MMAP:unloadMap: Unloaded mmtile {:04}[{:02}, {:02}] from {:04}", meshMapId, x, y, meshMapId); + } + } + + mmap->meshData.erase(meshItr); } TC_LOG_DEBUG("maps", "MMAP:unloadMapInstance: Unloaded mapId {:04} instanceId {}", instanceMapId, instanceId); - - return true; } - dtNavMesh const* MMapManager::GetNavMesh(uint32 mapId) + dtNavMesh* MMapManager::GetNavMesh(uint32 mapId, uint32 instanceId) { MMapDataSet::const_iterator itr = GetMMapData(mapId); if (itr == loadedMMaps.end()) return nullptr; - return itr->second->navMesh.get(); + MeshDataMap::iterator meshItr = itr->second->FindMeshData(mapId, instanceId); + if (meshItr == itr->second->meshData.end()) + return nullptr; + + return &meshItr->second.navMesh; } dtNavMeshQuery const* MMapManager::GetNavMeshQuery(uint32 meshMapId, uint32 instanceMapId, uint32 instanceId) @@ -414,6 +482,6 @@ namespace MMAP if (queryItr == itr->second->navMeshQueries.end()) return nullptr; - return queryItr->second.get(); + return &queryItr->second; } } diff --git a/src/common/Collision/Management/MMapManager.h b/src/common/mmaps_common/Management/MMapManager.h index b48b773555b..3d8a86ba2c0 100644 --- a/src/common/Collision/Management/MMapManager.h +++ b/src/common/mmaps_common/Management/MMapManager.h @@ -45,7 +45,7 @@ namespace MMAP // singleton class // holds all all access to mmap loading unloading and meshes - class TC_COMMON_API MMapManager + class TC_MMAPS_COMMON_API MMapManager { public: MMapManager(); @@ -59,26 +59,28 @@ namespace MMAP void InitializeThreadUnsafe(std::unordered_map<uint32, std::vector<uint32>> const& mapData); static LoadResult parseNavMeshParamsFile(std::string_view basePath, uint32 mapId, dtNavMeshParams* params, std::vector<OffMeshData>* offmeshConnections = nullptr); - LoadResult loadMap(std::string_view basePath, uint32 mapId, int32 x, int32 y); + LoadResult loadMap(std::string_view basePath, uint32 mapId, uint32 instanceId, int32 x, int32 y); bool loadMapInstance(std::string_view basePath, uint32 meshMapId, uint32 instanceMapId, uint32 instanceId); - bool unloadMap(uint32 mapId, int32 x, int32 y); - bool unloadMap(uint32 mapId); - bool unloadMapInstance(uint32 meshMapId, uint32 instanceMapId, uint32 instanceId); + void unloadMap(uint32 mapId, int32 x, int32 y); + void unloadMap(uint32 mapId); + void unloadMapInstance(uint32 meshMapId, uint32 instanceMapId, uint32 instanceId); // the returned [dtNavMeshQuery const*] is NOT threadsafe dtNavMeshQuery const* GetNavMeshQuery(uint32 meshMapId, uint32 instanceMapId, uint32 instanceId); - dtNavMesh const* GetNavMesh(uint32 mapId); + dtNavMesh* GetNavMesh(uint32 mapId, uint32 instanceId); uint32 getLoadedTilesCount() const { return loadedTiles; } uint32 getLoadedMapsCount() const { return uint32(loadedMMaps.size()); } + + static bool isRebuildingTilesEnabledOnMap(uint32 mapId); + private: - LoadResult loadMapData(std::string_view basePath, uint32 mapId); + LoadResult loadMapData(std::string_view basePath, uint32 mapId, uint32 instanceId); uint32 packTileID(int32 x, int32 y); MMapDataSet::const_iterator GetMMapData(uint32 mapId) const; MMapDataSet loadedMMaps; uint32 loadedTiles = 0; - bool thread_safe_environment = true; std::unordered_map<uint32, uint32> parentMapData; }; |
