aboutsummaryrefslogtreecommitdiff
path: root/src/common
diff options
context:
space:
mode:
Diffstat (limited to 'src/common')
-rw-r--r--src/common/CMakeLists.txt4
-rw-r--r--src/common/Collision/BoundingIntervalHierarchyWrapper.h3
-rw-r--r--src/common/Collision/DynamicTree.cpp8
-rw-r--r--src/common/Collision/DynamicTree.h3
-rw-r--r--src/common/Collision/Models/GameObjectModel.h21
-rw-r--r--src/common/Collision/RegularGrid.h7
-rw-r--r--src/common/Define.h6
-rw-r--r--src/common/mmaps_common/CMakeLists.txt52
-rw-r--r--src/common/mmaps_common/Generator/IntermediateValues.cpp252
-rw-r--r--src/common/mmaps_common/Generator/IntermediateValues.h78
-rw-r--r--src/common/mmaps_common/Generator/TerrainBuilder.cpp812
-rw-r--r--src/common/mmaps_common/Generator/TerrainBuilder.h132
-rw-r--r--src/common/mmaps_common/Generator/TileBuilder.cpp568
-rw-r--r--src/common/mmaps_common/Generator/TileBuilder.h100
-rw-r--r--src/common/mmaps_common/MMapDefines.h (renamed from src/common/Collision/Maps/MMapDefines.h)0
-rw-r--r--src/common/mmaps_common/Management/MMapManager.cpp (renamed from src/common/Collision/Management/MMapManager.cpp)236
-rw-r--r--src/common/mmaps_common/Management/MMapManager.h (renamed from src/common/Collision/Management/MMapManager.h)18
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(), &ltris[0], &ltris[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(&params, &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, &params); paramsResult != LoadResult::Success)
return paramsResult;
- NavMeshPtr mesh(dtAllocNavMesh());
- ASSERT(mesh);
- if (dtStatusFailed(mesh->init(&params)))
+ if (dtStatusFailed(meshItr->second.navMesh.init(&params)))
{
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;
};