diff options
| author | Shauren <shauren.trinity@gmail.com> | 2025-11-06 18:34:58 +0100 |
|---|---|---|
| committer | Shauren <shauren.trinity@gmail.com> | 2025-11-06 18:34:58 +0100 |
| commit | f439120b1f8c4f5a5c3fc0deb6aa01bf971e4fdf (patch) | |
| tree | 4eb1ec603a6ef879188dff1fc42566465d4c2f3e /src/common | |
| parent | 73c1a76ab5c031e74e543d7eb7bb59a9f20430d2 (diff) | |
Tools/mmaps_generator: Move TerrainBuilder to a shared project
Diffstat (limited to 'src/common')
| -rw-r--r-- | src/common/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/common/Define.h | 6 | ||||
| -rw-r--r-- | src/common/mmaps_common/CMakeLists.txt | 52 | ||||
| -rw-r--r-- | src/common/mmaps_common/Generator/IntermediateValues.cpp | 252 | ||||
| -rw-r--r-- | src/common/mmaps_common/Generator/IntermediateValues.h | 78 | ||||
| -rw-r--r-- | src/common/mmaps_common/Generator/TerrainBuilder.cpp | 812 | ||||
| -rw-r--r-- | src/common/mmaps_common/Generator/TerrainBuilder.h | 132 | ||||
| -rw-r--r-- | src/common/mmaps_common/Generator/TileBuilder.cpp | 568 | ||||
| -rw-r--r-- | src/common/mmaps_common/Generator/TileBuilder.h | 100 | ||||
| -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) | 0 | ||||
| -rw-r--r-- | src/common/mmaps_common/Management/MMapManager.h (renamed from src/common/Collision/Management/MMapManager.h) | 2 |
12 files changed, 2004 insertions, 2 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/Define.h b/src/common/Define.h index f918db84314..32862f71530 100644 --- a/src/common/Define.h +++ b/src/common/Define.h @@ -129,6 +129,12 @@ # define TC_GAME_API TC_API_IMPORT #endif +#ifdef TRINITY_API_EXPORT_MMAPS_COMMON +# define TC_MMAPS_COMMON_API TC_API_EXPORT +#else +# define TC_MMAPS_COMMON_API TC_API_IMPORT +#endif + #define UI64FMTD "%" PRIu64 #define UI64LIT(N) UINT64_C(N) diff --git a/src/common/mmaps_common/CMakeLists.txt b/src/common/mmaps_common/CMakeLists.txt new file mode 100644 index 00000000000..2a65e8d5963 --- /dev/null +++ b/src/common/mmaps_common/CMakeLists.txt @@ -0,0 +1,52 @@ +# This file is part of the TrinityCore Project. See AUTHORS file for Copyright information +# +# This file is free software; as a special exception the author gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +CollectSourceFiles( + ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE_SOURCES) + +GroupSources(${CMAKE_CURRENT_SOURCE_DIR}) + +CollectIncludeDirectories( + ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC_INCLUDES +) + +add_library(mmaps_common ${PRIVATE_SOURCES}) + +target_link_libraries(mmaps_common + PRIVATE + trinity-core-interface + PUBLIC + common + Recast + Detour) + +target_include_directories(mmaps_common + PUBLIC + ${PUBLIC_INCLUDES}) + +set_target_properties(mmaps_common + PROPERTIES + COMPILE_WARNING_AS_ERROR ${WITH_WARNINGS_AS_ERRORS} + DEFINE_SYMBOL TRINITY_API_EXPORT_MMAPS_COMMON + FOLDER "tools") + +if(BUILD_SHARED_LIBS) + if(UNIX) + install(TARGETS mmaps_common + LIBRARY + DESTINATION lib) + elseif(WIN32) + install(TARGETS mmaps_common + RUNTIME + DESTINATION "${CMAKE_INSTALL_PREFIX}") + endif() +endif() diff --git a/src/common/mmaps_common/Generator/IntermediateValues.cpp b/src/common/mmaps_common/Generator/IntermediateValues.cpp new file mode 100644 index 00000000000..858ac25d418 --- /dev/null +++ b/src/common/mmaps_common/Generator/IntermediateValues.cpp @@ -0,0 +1,252 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "IntermediateValues.h" +#include "Log.h" +#include "Memory.h" +#include "StringFormat.h" + +namespace MMAP +{ + IntermediateValues::~IntermediateValues() + { + rcFreeCompactHeightfield(compactHeightfield); + rcFreeHeightField(heightfield); + rcFreeContourSet(contours); + rcFreePolyMesh(polyMesh); + rcFreePolyMeshDetail(polyMeshDetail); + } + + void IntermediateValues::writeIV(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY) + { + TC_LOG_INFO("maps.mmapgen.debug", "[Map {:04}] [{:02},{:02}]: Writing debug output intermediate values...", mapID, tileX, tileY); + + auto debugWrite = [=, outputDirectory = outputDirectory.generic_string()](char const* extension, auto const* data) + { + std::string fileName = Trinity::StringFormat("{}/meshes/{:04}_{:02}_{:02}{}.{}", outputDirectory, mapID, tileX, tileY, fileNameSuffix, extension); + if (auto file = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(fileName.c_str(), "wb"))) + { + IntermediateValues::debugWrite(file.get(), data); + } + else + TC_LOG_ERROR("maps.mmapgen.debug", "{}: [{:04}-{:02},{:02}] Failed to open {} for writing!", strerror(errno), mapID, tileX, tileY, fileName); + }; + + if (heightfield) + debugWrite("hf", heightfield); + if (compactHeightfield) + debugWrite("chf", compactHeightfield); + if (contours) + debugWrite("cs", contours); + if (polyMesh) + debugWrite("pmesh", polyMesh); + if (polyMeshDetail) + debugWrite("dmesh", polyMeshDetail); + } + + void IntermediateValues::debugWrite(FILE* file, rcHeightfield const* mesh) + { + if (!file || !mesh) + return; + + fwrite(&(mesh->cs), sizeof(float), 1, file); + fwrite(&(mesh->ch), sizeof(float), 1, file); + fwrite(&(mesh->width), sizeof(int), 1, file); + fwrite(&(mesh->height), sizeof(int), 1, file); + fwrite(mesh->bmin, sizeof(float), 3, file); + fwrite(mesh->bmax, sizeof(float), 3, file); + + for (int y = 0; y < mesh->height; ++y) + for (int x = 0; x < mesh->width; ++x) + { + rcSpan* span = mesh->spans[x+y*mesh->width]; + + // first, count the number of spans + int spanCount = 0; + while (span) + { + spanCount++; + span = span->next; + } + + // write the span count + fwrite(&spanCount, sizeof(int), 1, file); + + // write the spans + span = mesh->spans[x+y*mesh->width]; + while (span) + { + fwrite(span, sizeof(rcSpan), 1, file); + span = span->next; + } + } + } + + void IntermediateValues::debugWrite(FILE* file, rcCompactHeightfield const* chf) + { + if (!file | !chf) + return; + + fwrite(&(chf->width), sizeof(chf->width), 1, file); + fwrite(&(chf->height), sizeof(chf->height), 1, file); + fwrite(&(chf->spanCount), sizeof(chf->spanCount), 1, file); + + fwrite(&(chf->walkableHeight), sizeof(chf->walkableHeight), 1, file); + fwrite(&(chf->walkableClimb), sizeof(chf->walkableClimb), 1, file); + + fwrite(&(chf->maxDistance), sizeof(chf->maxDistance), 1, file); + fwrite(&(chf->maxRegions), sizeof(chf->maxRegions), 1, file); + + fwrite(chf->bmin, sizeof(chf->bmin), 1, file); + fwrite(chf->bmax, sizeof(chf->bmax), 1, file); + + fwrite(&(chf->cs), sizeof(chf->cs), 1, file); + fwrite(&(chf->ch), sizeof(chf->ch), 1, file); + + int tmp = 0; + if (chf->cells) tmp |= 1; + if (chf->spans) tmp |= 2; + if (chf->dist) tmp |= 4; + if (chf->areas) tmp |= 8; + + fwrite(&tmp, sizeof(tmp), 1, file); + + if (chf->cells) + fwrite(chf->cells, sizeof(rcCompactCell), chf->width*chf->height, file); + if (chf->spans) + fwrite(chf->spans, sizeof(rcCompactSpan), chf->spanCount, file); + if (chf->dist) + fwrite(chf->dist, sizeof(unsigned short), chf->spanCount, file); + if (chf->areas) + fwrite(chf->areas, sizeof(unsigned char), chf->spanCount, file); + } + + void IntermediateValues::debugWrite(FILE* file, rcContourSet const* cs) + { + if (!file || !cs) + return; + + fwrite(&(cs->cs), sizeof(float), 1, file); + fwrite(&(cs->ch), sizeof(float), 1, file); + fwrite(cs->bmin, sizeof(float), 3, file); + fwrite(cs->bmax, sizeof(float), 3, file); + fwrite(&(cs->nconts), sizeof(int), 1, file); + for (int i = 0; i < cs->nconts; ++i) + { + fwrite(&cs->conts[i].area, sizeof(unsigned char), 1, file); + fwrite(&cs->conts[i].reg, sizeof(unsigned short), 1, file); + fwrite(&cs->conts[i].nverts, sizeof(int), 1, file); + fwrite(cs->conts[i].verts, sizeof(int), cs->conts[i].nverts*4, file); + fwrite(&cs->conts[i].nrverts, sizeof(int), 1, file); + fwrite(cs->conts[i].rverts, sizeof(int), cs->conts[i].nrverts*4, file); + } + } + + void IntermediateValues::debugWrite(FILE* file, rcPolyMesh const* mesh) + { + if (!file || !mesh) + return; + + fwrite(&(mesh->cs), sizeof(float), 1, file); + fwrite(&(mesh->ch), sizeof(float), 1, file); + fwrite(&(mesh->nvp), sizeof(int), 1, file); + fwrite(mesh->bmin, sizeof(float), 3, file); + fwrite(mesh->bmax, sizeof(float), 3, file); + fwrite(&(mesh->nverts), sizeof(int), 1, file); + fwrite(mesh->verts, sizeof(unsigned short), mesh->nverts*3, file); + fwrite(&(mesh->npolys), sizeof(int), 1, file); + fwrite(mesh->polys, sizeof(unsigned short), mesh->npolys*mesh->nvp*2, file); + fwrite(mesh->flags, sizeof(unsigned short), mesh->npolys, file); + fwrite(mesh->areas, sizeof(unsigned char), mesh->npolys, file); + fwrite(mesh->regs, sizeof(unsigned short), mesh->npolys, file); + } + + void IntermediateValues::debugWrite(FILE* file, rcPolyMeshDetail const* mesh) + { + if (!file || !mesh) + return; + + fwrite(&(mesh->nverts), sizeof(int), 1, file); + fwrite(mesh->verts, sizeof(float), mesh->nverts*3, file); + fwrite(&(mesh->ntris), sizeof(int), 1, file); + fwrite(mesh->tris, sizeof(char), mesh->ntris*4, file); + fwrite(&(mesh->nmeshes), sizeof(int), 1, file); + fwrite(mesh->meshes, sizeof(int), mesh->nmeshes*4, file); + } + + void IntermediateValues::generateObjFile(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY, MeshData const& meshData) + { + std::string objFileName; + objFileName = Trinity::StringFormat("{}/meshes/map{:04}_{:02}_{:02}{}.obj", outputDirectory.generic_string(), mapID, tileX, tileY, fileNameSuffix); + + auto objFile = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(objFileName.c_str(), "wb")); + if (!objFile) + { + TC_LOG_ERROR("maps.mmapgen.debug", "{}: Failed to open {} for writing!", strerror(errno), objFileName); + return; + } + + std::vector<float> allVerts; + std::vector<int> allTris; + + allTris.insert(allTris.end(), meshData.liquidTris.begin(), meshData.liquidTris.end()); + allVerts.insert(allVerts.end(), meshData.liquidVerts.begin(), meshData.liquidVerts.end()); + TerrainBuilder::copyIndices(meshData.solidTris, allTris, allVerts.size() / 3); + allVerts.insert(allVerts.end(), meshData.solidVerts.begin(), meshData.solidVerts.end()); + + float* verts = allVerts.data(); + int vertCount = allVerts.size() / 3; + int* tris = allTris.data(); + int triCount = allTris.size() / 3; + + for (std::size_t i = 0; i < allVerts.size() / 3; i++) + fprintf(objFile.get(), "v %f %f %f\n", verts[i * 3], verts[i * 3 + 1], verts[i * 3 + 2]); + + for (std::size_t i = 0; i < allTris.size() / 3; i++) + fprintf(objFile.get(), "f %i %i %i\n", tris[i * 3] + 1, tris[i * 3 + 1] + 1, tris[i * 3 + 2] + 1); + + TC_LOG_INFO("maps.mmapgen.debug", "[Map {:04}] [{:02},{:02}]: Writing debug output object file...", mapID, tileX, tileY); + + objFileName = Trinity::StringFormat("{}/meshes/map{:04}.map", outputDirectory.generic_string(), mapID); + + objFile.reset(fopen(objFileName.c_str(), "wb")); + if (!objFile) + { + TC_LOG_ERROR("maps.mmapgen.debug", "{}: Failed to open {} for writing!", strerror(errno), objFileName); + return; + } + + char b = '\0'; + fwrite(&b, sizeof(char), 1, objFile.get()); + + objFileName = Trinity::StringFormat("{}/meshes/map{:04}_{:02}_{:02}{}.mesh", outputDirectory.generic_string(), mapID, tileX, tileY, fileNameSuffix); + objFile.reset(fopen(objFileName.c_str(), "wb")); + if (!objFile) + { + TC_LOG_ERROR("maps.mmapgen.debug", "{}: Failed to open {} for writing!", strerror(errno), objFileName); + return; + } + + fwrite(&vertCount, sizeof(int), 1, objFile.get()); + fwrite(verts, sizeof(float), vertCount*3, objFile.get()); + fflush(objFile.get()); + + fwrite(&triCount, sizeof(int), 1, objFile.get()); + fwrite(tris, sizeof(int), triCount*3, objFile.get()); + fflush(objFile.get()); + } +} diff --git a/src/common/mmaps_common/Generator/IntermediateValues.h b/src/common/mmaps_common/Generator/IntermediateValues.h new file mode 100644 index 00000000000..f8578683e61 --- /dev/null +++ b/src/common/mmaps_common/Generator/IntermediateValues.h @@ -0,0 +1,78 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _INTERMEDIATE_VALUES_H +#define _INTERMEDIATE_VALUES_H + +#include "TerrainBuilder.h" +#include <Recast.h> +#include <utility> + +namespace MMAP +{ + // this class gathers all debug info holding and output + struct IntermediateValues + { + rcHeightfield* heightfield; + rcCompactHeightfield* compactHeightfield; + rcContourSet* contours; + rcPolyMesh* polyMesh; + rcPolyMeshDetail* polyMeshDetail; + + IntermediateValues() : heightfield(nullptr), compactHeightfield(nullptr), + contours(nullptr), polyMesh(nullptr), polyMeshDetail(nullptr) {} + + IntermediateValues(IntermediateValues const&) = delete; + + IntermediateValues(IntermediateValues&& other) noexcept : + heightfield(std::exchange(other.heightfield, nullptr)), + compactHeightfield(std::exchange(other.compactHeightfield, nullptr)), + contours(std::exchange(other.contours, nullptr)), + polyMesh(std::exchange(other.polyMesh, nullptr)), + polyMeshDetail(std::exchange(other.polyMeshDetail, nullptr)) + { + } + + ~IntermediateValues(); + + IntermediateValues& operator=(IntermediateValues const&) = delete; + + IntermediateValues& operator=(IntermediateValues&& other) noexcept + { + if (this != std::addressof(other)) + { + heightfield = std::exchange(other.heightfield, nullptr); + compactHeightfield = std::exchange(other.compactHeightfield, nullptr); + contours = std::exchange(other.contours, nullptr); + polyMesh = std::exchange(other.polyMesh, nullptr); + polyMeshDetail = std::exchange(other.polyMeshDetail, nullptr); + } + return *this; + } + + void writeIV(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY); + + static void debugWrite(FILE* file, rcHeightfield const* mesh); + static void debugWrite(FILE* file, rcCompactHeightfield const* chf); + static void debugWrite(FILE* file, rcContourSet const* cs); + static void debugWrite(FILE* file, rcPolyMesh const* mesh); + static void debugWrite(FILE* file, rcPolyMeshDetail const* mesh); + + void generateObjFile(boost::filesystem::path const& outputDirectory, std::string_view fileNameSuffix, uint32 mapID, uint32 tileX, uint32 tileY, MeshData const& meshData); + }; +} +#endif diff --git a/src/common/mmaps_common/Generator/TerrainBuilder.cpp b/src/common/mmaps_common/Generator/TerrainBuilder.cpp new file mode 100644 index 00000000000..449a5cb7557 --- /dev/null +++ b/src/common/mmaps_common/Generator/TerrainBuilder.cpp @@ -0,0 +1,812 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "TerrainBuilder.h" +#include "Log.h" +#include "MapDefines.h" +#include "MapTree.h" +#include "MMapDefines.h" +#include "Memory.h" +#include "ModelInstance.h" +#include "StringFormat.h" +#include "Util.h" +#include "VMapManager.h" +#include <unordered_map> + +namespace MMAP +{ + std::unique_ptr<VMAP::VMapManager> (*CreateVMapManager)(uint32 mapId); + + TerrainBuilder::TerrainBuilder(boost::filesystem::path const& inputDirectory, bool skipLiquid) : + m_inputDirectory(inputDirectory), + m_skipLiquid (skipLiquid) + { + } + + /**************************************************************************/ + void TerrainBuilder::getLoopVars(Spot portion, int& loopStart, int& loopEnd, int& loopInc) + { + switch (portion) + { + case ENTIRE: + loopStart = 0; + loopEnd = V8_SIZE_SQ; + loopInc = 1; + break; + case TOP: + loopStart = 0; + loopEnd = V8_SIZE; + loopInc = 1; + break; + case LEFT: + loopStart = 0; + loopEnd = V8_SIZE_SQ - V8_SIZE + 1; + loopInc = V8_SIZE; + break; + case RIGHT: + loopStart = V8_SIZE - 1; + loopEnd = V8_SIZE_SQ; + loopInc = V8_SIZE; + break; + case BOTTOM: + loopStart = V8_SIZE_SQ - V8_SIZE; + loopEnd = V8_SIZE_SQ; + loopInc = 1; + break; + } + } + + /**************************************************************************/ + void TerrainBuilder::loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager) + { + if (loadMap(mapID, tileX, tileY, meshData, vmapManager, ENTIRE)) + { + loadMap(mapID, tileX, tileY+1, meshData, vmapManager, LEFT); + loadMap(mapID, tileX, tileY-1, meshData, vmapManager, RIGHT); + loadMap(mapID, tileX+1, tileY, meshData, vmapManager, TOP); + loadMap(mapID, tileX-1, tileY, meshData, vmapManager, BOTTOM); + } + } + + /**************************************************************************/ + bool TerrainBuilder::loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager, Spot portion) + { + std::string mapFileName = Trinity::StringFormat("{}/maps/{:04}_{:02}_{:02}.map", m_inputDirectory.generic_string(), mapID, tileX, tileY); + + auto mapFile = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(mapFileName.c_str(), "rb")); + if (!mapFile) + { + int32 parentMapId = vmapManager->getParentMapId(mapID); + while (!mapFile && parentMapId != -1) + { + mapFileName = Trinity::StringFormat("{}/maps/{:04}_{:02}_{:02}.map", m_inputDirectory.generic_string(), parentMapId, tileX, tileY); + mapFile.reset(fopen(mapFileName.c_str(), "rb")); + parentMapId = vmapManager->getParentMapId(parentMapId); + } + } + + if (!mapFile) + return false; + + map_fileheader fheader; + if (fread(&fheader, sizeof(map_fileheader), 1, mapFile.get()) != 1 || + fheader.versionMagic != MapVersionMagic) + { + TC_LOG_ERROR("maps.mmapgen", "{} is the wrong version, please extract new .map files", mapFileName); + return false; + } + + map_heightHeader hheader; + fseek(mapFile.get(), fheader.heightMapOffset, SEEK_SET); + + bool haveTerrain = false; + bool haveLiquid = false; + if (fread(&hheader, sizeof(map_heightHeader), 1, mapFile.get()) == 1) + { + haveTerrain = !hheader.flags.HasFlag(map_heightHeaderFlags::NoHeight); + haveLiquid = fheader.liquidMapOffset && !m_skipLiquid; + } + + // no data in this map file + if (!haveTerrain && !haveLiquid) + return false; + + // data used later + uint8 holes[16][16][8] = { }; + uint16 liquid_entry[16][16] = { }; + map_liquidHeaderTypeFlags liquid_flags[16][16] = { }; + std::vector<int> ltriangles; + std::vector<int> ttriangles; + + // terrain data + if (haveTerrain) + { + float heightMultiplier; + float V9[V9_SIZE_SQ], V8[V8_SIZE_SQ]; + size_t expected = V9_SIZE_SQ + V8_SIZE_SQ; + + if (hheader.flags.HasFlag(map_heightHeaderFlags::HeightAsInt8)) + { + uint8 v9[V9_SIZE_SQ]; + uint8 v8[V8_SIZE_SQ]; + size_t count = 0; + count += fread(v9, sizeof(uint8), V9_SIZE_SQ, mapFile.get()); + count += fread(v8, sizeof(uint8), V8_SIZE_SQ, mapFile.get()); + if (count != expected) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} height data expected {}, read {}", mapFileName, expected, count); + + heightMultiplier = (hheader.gridMaxHeight - hheader.gridHeight) / 255; + + for (int i = 0; i < V9_SIZE_SQ; ++i) + V9[i] = (float)v9[i]*heightMultiplier + hheader.gridHeight; + + for (int i = 0; i < V8_SIZE_SQ; ++i) + V8[i] = (float)v8[i]*heightMultiplier + hheader.gridHeight; + } + else if (hheader.flags.HasFlag(map_heightHeaderFlags::HeightAsInt16)) + { + uint16 v9[V9_SIZE_SQ]; + uint16 v8[V8_SIZE_SQ]; + size_t count = 0; + count += fread(v9, sizeof(uint16), V9_SIZE_SQ, mapFile.get()); + count += fread(v8, sizeof(uint16), V8_SIZE_SQ, mapFile.get()); + if (count != expected) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} height data expected {}, read {}", mapFileName, expected, count); + + heightMultiplier = (hheader.gridMaxHeight - hheader.gridHeight) / 65535; + + for (int i = 0; i < V9_SIZE_SQ; ++i) + V9[i] = (float)v9[i]*heightMultiplier + hheader.gridHeight; + + for (int i = 0; i < V8_SIZE_SQ; ++i) + V8[i] = (float)v8[i]*heightMultiplier + hheader.gridHeight; + } + else + { + size_t count = 0; + count += fread(V9, sizeof(float), V9_SIZE_SQ, mapFile.get()); + count += fread(V8, sizeof(float), V8_SIZE_SQ, mapFile.get()); + if (count != expected) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} height data expected {}, read {}", mapFileName, expected, count); + } + + // hole data + if (fheader.holesSize != 0) + { + memset(holes, 0, fheader.holesSize); + fseek(mapFile.get(), fheader.holesOffset, SEEK_SET); + if (fread(holes, fheader.holesSize, 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} holes data expected {}, read {}", mapFileName, 1, 0); + } + + int count = meshData.solidVerts.size() / 3; + meshData.solidVerts.resize(meshData.solidVerts.size() + (V9_SIZE_SQ + V8_SIZE_SQ) * 3); + float* solidVerts = meshData.solidVerts.data() + count * 3; + + float xoffset = (float(tileX)-32)*GRID_SIZE; + float yoffset = (float(tileY)-32)*GRID_SIZE; + + for (int i = 0; i < V9_SIZE_SQ; ++i) + { + getHeightCoord(i, GRID_V9, xoffset, yoffset, solidVerts, V9); + solidVerts += 3; + } + + for (int i = 0; i < V8_SIZE_SQ; ++i) + { + getHeightCoord(i, GRID_V8, xoffset, yoffset, solidVerts, V8); + solidVerts += 3; + } + + int loopStart = 0, loopEnd = 0, loopInc = 0; + getLoopVars(portion, loopStart, loopEnd, loopInc); + for (int i = loopStart; i < loopEnd; i+=loopInc) + { + std::size_t trianglesOffset = ttriangles.size(); + ttriangles.resize(ttriangles.size() + 12); + getHeightTriangle(i, TOP, ttriangles.data() + trianglesOffset + 0, count); + getHeightTriangle(i, RIGHT, ttriangles.data() + trianglesOffset + 3, count); + getHeightTriangle(i, LEFT, ttriangles.data() + trianglesOffset + 6, count); + getHeightTriangle(i, BOTTOM, ttriangles.data() + trianglesOffset + 9, count); + } + } + + // liquid data + if (haveLiquid) + { + map_liquidHeader lheader; + fseek(mapFile.get(), fheader.liquidMapOffset, SEEK_SET); + if (fread(&lheader, sizeof(map_liquidHeader), 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid header expected {}, read {}", mapFileName, 1, 0); + + std::unique_ptr<float[]> liquid_map; + + if (!lheader.flags.HasFlag(map_liquidHeaderFlags::NoType)) + { + if (fread(liquid_entry, sizeof(liquid_entry), 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid id expected {}, read {}", mapFileName, 1, 0); + if (fread(liquid_flags, sizeof(liquid_flags), 1, mapFile.get()) != 1) + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid flags expected {}, read {}", mapFileName, 1, 0); + } + else + { + std::fill_n(&liquid_entry[0][0], 16 * 16, lheader.liquidType); + std::fill_n(&liquid_flags[0][0], 16 * 16, lheader.liquidFlags); + } + + if (!lheader.flags.HasFlag(map_liquidHeaderFlags::NoHeight)) + { + uint32 toRead = lheader.width * lheader.height; + liquid_map = std::make_unique<float[]>(toRead); + if (fread(liquid_map.get(), sizeof(float), toRead, mapFile.get()) != toRead) + { + TC_LOG_ERROR("maps.mmapgen", "TerrainBuilder::loadMap: Failed to read {} liquid header expected {}, read {}", mapFileName, toRead, 0); + liquid_map = nullptr; + } + } + + int count = meshData.liquidVerts.size() / 3; + meshData.liquidVerts.resize(meshData.liquidVerts.size() + V9_SIZE_SQ * 3); + float* liquidVerts = meshData.liquidVerts.data(); + float xoffset = (float(tileX)-32)*GRID_SIZE; + float yoffset = (float(tileY)-32)*GRID_SIZE; + + int row, col; + + // generate coordinates + if (!lheader.flags.HasFlag(map_liquidHeaderFlags::NoHeight)) + { + int j = 0; + for (int i = 0; i < V9_SIZE_SQ; ++i) + { + row = i / V9_SIZE; + col = i % V9_SIZE; + + if (row < lheader.offsetY || row >= lheader.offsetY + lheader.height || + col < lheader.offsetX || col >= lheader.offsetX + lheader.width) + { + // dummy vert using invalid height + liquidVerts[(count + i) * 3 + 0] = (yoffset + col * GRID_PART_SIZE) * -1; + liquidVerts[(count + i) * 3 + 1] = INVALID_MAP_LIQ_HEIGHT; + liquidVerts[(count + i) * 3 + 2] = (xoffset + row * GRID_PART_SIZE) * -1; + continue; + } + + getLiquidCoord(i, j, xoffset, yoffset, &liquidVerts[(count + i) * 3], liquid_map.get()); + j++; + } + } + else + { + for (int i = 0; i < V9_SIZE_SQ; ++i) + { + row = i / V9_SIZE; + col = i % V9_SIZE; + liquidVerts[(count + i) * 3 + 0] = (yoffset + col * GRID_PART_SIZE) * -1; + liquidVerts[(count + i) * 3 + 1] = lheader.liquidLevel; + liquidVerts[(count + i) * 3 + 2] = (xoffset + row * GRID_PART_SIZE) * -1; + } + } + + int loopStart = 0, loopEnd = 0, loopInc = 0; + getLoopVars(portion, loopStart, loopEnd, loopInc); + + // generate triangles + for (int i = loopStart; i < loopEnd; i += loopInc) + { + std::size_t trianglesOffset = ltriangles.size(); + ltriangles.resize(ltriangles.size() + 6); + getHeightTriangle(i, TOP, ltriangles.data() + trianglesOffset + 0, count, true); + getHeightTriangle(i, BOTTOM, ltriangles.data() + trianglesOffset + 3, count, true); + } + } + + // now that we have gathered the data, we can figure out which parts to keep: + // liquid above ground, ground above liquid + int loopStart = 0, loopEnd = 0, loopInc = 0, tTriCount = 4; + bool useTerrain, useLiquid; + + float* lverts = meshData.liquidVerts.data(); + int* ltris = ltriangles.data(); + + float* tverts = meshData.solidVerts.data(); + int* ttris = ttriangles.data(); + + if ((ltriangles.size() + ttriangles.size()) == 0) + return false; + + // make a copy of liquid vertices + // used to pad right-bottom frame due to lost vertex data at extraction + std::unique_ptr<float[]> lverts_copy; + if (!meshData.liquidVerts.empty()) + { + lverts_copy = std::make_unique<float[]>(meshData.liquidVerts.size()); + memcpy(lverts_copy.get(), lverts, sizeof(float)* meshData.liquidVerts.size()); + } + + getLoopVars(portion, loopStart, loopEnd, loopInc); + for (int i = loopStart; i < loopEnd; i+=loopInc) + { + for (int j = 0; j < 2; ++j) + { + // default is true, will change to false if needed + useTerrain = true; + useLiquid = true; + EnumFlag<map_liquidHeaderTypeFlags> liquidType = map_liquidHeaderTypeFlags::NoWater; + uint8 navLiquidType = NAV_AREA_EMPTY; + + // if there is no liquid, don't use liquid + if (meshData.liquidVerts.empty() || ltriangles.empty()) + useLiquid = false; + else + { + liquidType = getLiquidType(i, liquid_flags); + if (liquidType.HasFlag(map_liquidHeaderTypeFlags::DarkWater)) + { + // players should not be here, so logically neither should creatures + useTerrain = false; + useLiquid = false; + } + else if (liquidType.HasFlag(map_liquidHeaderTypeFlags::Water | map_liquidHeaderTypeFlags::Ocean)) + navLiquidType = NAV_AREA_WATER; + else if (liquidType.HasFlag(map_liquidHeaderTypeFlags::Magma | map_liquidHeaderTypeFlags::Slime)) + navLiquidType = NAV_AREA_MAGMA_SLIME; + else + useLiquid = false; + } + + // if there is no terrain, don't use terrain + if (ttriangles.empty()) + useTerrain = false; + + // while extracting ADT data we are losing right-bottom vertices + // this code adds fair approximation of lost data + if (useLiquid) + { + float quadHeight = 0; + uint32 validCount = 0; + for(uint32 idx = 0; idx < 3; idx++) + { + float h = lverts_copy[ltris[idx]*3 + 1]; + if (h != INVALID_MAP_LIQ_HEIGHT && h < INVALID_MAP_LIQ_HEIGHT_MAX) + { + quadHeight += h; + validCount++; + } + } + + // update vertex height data + if (validCount > 0 && validCount < 3) + { + quadHeight /= validCount; + for(uint32 idx = 0; idx < 3; idx++) + { + float h = lverts[ltris[idx]*3 + 1]; + if (h == INVALID_MAP_LIQ_HEIGHT || h > INVALID_MAP_LIQ_HEIGHT_MAX) + lverts[ltris[idx]*3 + 1] = quadHeight; + } + } + + // no valid vertexes - don't use this poly at all + if (validCount == 0) + useLiquid = false; + } + + // if there is a hole here, don't use the terrain + if (useTerrain && fheader.holesSize != 0) + useTerrain = !isHole(i, holes); + + // we use only one terrain kind per quad - pick higher one + if (useTerrain && useLiquid) + { + float minLLevel = INVALID_MAP_LIQ_HEIGHT_MAX; + float maxLLevel = INVALID_MAP_LIQ_HEIGHT; + for(uint32 x = 0; x < 3; x++) + { + float h = lverts[ltris[x]*3 + 1]; + if (minLLevel > h) + minLLevel = h; + + if (maxLLevel < h) + maxLLevel = h; + } + + float maxTLevel = INVALID_MAP_LIQ_HEIGHT; + float minTLevel = INVALID_MAP_LIQ_HEIGHT_MAX; + for(uint32 x = 0; x < 6; x++) + { + float h = tverts[ttris[x]*3 + 1]; + if (maxTLevel < h) + maxTLevel = h; + + if (minTLevel > h) + minTLevel = h; + } + + // terrain under the liquid? + if (minLLevel > maxTLevel) + useTerrain = false; + + //liquid under the terrain? + if (minTLevel > maxLLevel) + useLiquid = false; + } + + // store the result + if (useLiquid) + { + meshData.liquidTris.insert(meshData.liquidTris.end(), <ris[0], <ris[3]); + meshData.liquidType.push_back(navLiquidType); + } + + if (useTerrain) + meshData.solidTris.insert(meshData.solidTris.end(), &ttris[0], &ttris[3 * tTriCount / 2]); + + // advance to next set of triangles + ltris += 3; + ttris += 3*tTriCount/2; + } + } + + return !meshData.solidTris.empty() || !meshData.liquidTris.empty(); + } + + /**************************************************************************/ + inline void TerrainBuilder::getHeightCoord(int index, Grid grid, float xOffset, float yOffset, float* coord, float* v) + { + // wow coords: x, y, height + // coord is mirroed about the horizontal axes + switch (grid) + { + case GRID_V9: + coord[0] = (yOffset + (int)(index % V9_SIZE) * GRID_PART_SIZE) * -1.f; + coord[1] = v[index]; + coord[2] = (xOffset + (int)(index / (V9_SIZE)) * GRID_PART_SIZE) * -1.f; + break; + case GRID_V8: + coord[0] = (yOffset + (int)(index % V8_SIZE) * GRID_PART_SIZE + GRID_PART_SIZE / 2.f) * -1.f; + coord[1] = v[index]; + coord[2] = (xOffset + (int)(index / (V8_SIZE)) * GRID_PART_SIZE + GRID_PART_SIZE / 2.f) * -1.f; + break; + } + } + + /**************************************************************************/ + inline void TerrainBuilder::getHeightTriangle(int square, Spot triangle, int* indices, int offset, bool liquid/* = false*/) + { + int rowOffset = square/V8_SIZE; + if (!liquid) + switch (triangle) + { + case TOP: + indices[0] = V9_SIZE_SQ + square + offset; // 0-----1 .... 128 + indices[1] = square + 1 + rowOffset + offset; // |\ T /| + indices[2] = square + rowOffset + offset; // | \ / | + break; // |L 0 R| .. 127 + case LEFT: // | / \ | + indices[0] = square + V9_SIZE + rowOffset + offset; // |/ B \| + indices[1] = V9_SIZE_SQ + square + offset; // 129---130 ... 386 + indices[2] = square + rowOffset + offset; // |\ /| + break; // | \ / | + case RIGHT: // | 128 | .. 255 + indices[0] = V9_SIZE_SQ + square + offset; // | / \ | + indices[1] = square + V9_SIZE + 1 + rowOffset + offset; // |/ \| + indices[2] = square + 1 + rowOffset + offset; // 258---259 ... 515 + break; + case BOTTOM: + indices[0] = square + V9_SIZE + rowOffset + offset; + indices[1] = square + V9_SIZE + 1 + rowOffset + offset; + indices[2] = V9_SIZE_SQ + square + offset; + break; + default: break; + } + else + switch (triangle) + { // 0-----1 .... 128 + case TOP: // |\ | + indices[0] = square + V9_SIZE + 1 + rowOffset + offset; // | \ T | + indices[1] = square + 1 + rowOffset + offset; // | \ | + indices[2] = square + rowOffset + offset; // | B \ | + break; // | \| + case BOTTOM: // 129---130 ... 386 + indices[0] = square + V9_SIZE + rowOffset + offset; // |\ | + indices[1] = square + V9_SIZE + 1 + rowOffset + offset; // | \ | + indices[2] = square + rowOffset + offset; // | \ | + break; // | \ | + default: break; // | \| + } // 258---259 ... 515 + + } + + /**************************************************************************/ + inline void TerrainBuilder::getLiquidCoord(int index, int index2, float xOffset, float yOffset, float* coord, float* v) + { + // wow coords: x, y, height + // coord is mirroed about the horizontal axes + coord[0] = (yOffset + (int)(index % V9_SIZE) * GRID_PART_SIZE) * -1.f; + coord[1] = v[index2]; + coord[2] = (xOffset + (int)(index / (V9_SIZE)) * GRID_PART_SIZE) * -1.f; + } + + /**************************************************************************/ + inline bool TerrainBuilder::isHole(int square, uint8 const (&holes)[16][16][8]) + { + int row = square / 128; + int col = square % 128; + int cellRow = row / 8; // 8 squares per cell + int cellCol = col / 8; + int holeRow = row % 8; + int holeCol = col % 8; + + return (holes[cellRow][cellCol][holeRow] & (1 << holeCol)) != 0; + } + + /**************************************************************************/ + inline map_liquidHeaderTypeFlags TerrainBuilder::getLiquidType(int square, map_liquidHeaderTypeFlags const (&liquid_type)[16][16]) + { + int row = square / 128; + int col = square % 128; + int cellRow = row / 8; // 8 squares per cell + int cellCol = col / 8; + + return liquid_type[cellRow][cellCol]; + } + + /**************************************************************************/ + bool TerrainBuilder::loadVMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager) + { + VMAP::LoadResult result = vmapManager->loadMap((m_inputDirectory / "vmaps").string(), mapID, tileX, tileY); + if (result != VMAP::LoadResult::Success) + return false; + + auto vmapTile = Trinity::make_unique_ptr_with_deleter(vmapManager, [=](VMAP::VMapManager* mgr) + { + mgr->unloadMap(mapID, tileX, tileY); + }); + + std::span<VMAP::ModelInstance const> models = vmapManager->getModelsOnMap(mapID); + if (models.empty()) + return false; + + bool retval = false; + + for (VMAP::ModelInstance const& instance : models) + { + // model instances exist in tree even though there are instances of that model in this tile + VMAP::WorldModel const* worldModel = instance.getWorldModel(); + if (!worldModel) + continue; + + // now we have a model to add to the meshdata + retval = true; + + G3D::Vector3 position = instance.iPos; + position.x -= 32 * GRID_SIZE; + position.y -= 32 * GRID_SIZE; + loadVMapModel(worldModel, position, instance.GetInvRot(), instance.iScale, meshData, vmapManager); + } + + return retval; + } + + void TerrainBuilder::loadVMapModel(VMAP::WorldModel const* worldModel, G3D::Vector3 const& position, G3D::Matrix3 const& rotation, float scale, + MeshData& meshData, VMAP::VMapManager* vmapManager) + { + std::vector<VMAP::GroupModel> const& groupModels = worldModel->getGroupModels(); + + // all M2s need to have triangle indices reversed + bool isM2 = worldModel->IsM2(); + + // transform data + for (std::vector<VMAP::GroupModel>::const_iterator it = groupModels.begin(); it != groupModels.end(); ++it) + { + // first handle collision mesh + int offset = meshData.solidVerts.size() / 3; + transformVertices(it->GetVertices(), meshData.solidVerts, scale, rotation, position); + copyIndices(it->GetTriangles(), meshData.solidTris, offset, isM2); + + // now handle liquid data + VMAP::WmoLiquid const* liquid = it->GetLiquid(); + if (liquid && liquid->GetFlagsStorage()) + { + uint32 tilesX, tilesY; + G3D::Vector3 corner; + liquid->getPosInfo(tilesX, tilesY, corner); + uint32 vertsX = tilesX + 1; + uint32 vertsY = tilesY + 1; + uint8 const* flags = liquid->GetFlagsStorage(); + float const* data = liquid->GetHeightStorage(); + uint8 type = NAV_AREA_EMPTY; + + // convert liquid type to NavTerrain + EnumFlag<map_liquidHeaderTypeFlags> liquidFlags = map_liquidHeaderTypeFlags(vmapManager->GetLiquidFlagsPtr(liquid->GetType())); + if (liquidFlags.HasFlag(map_liquidHeaderTypeFlags::Water | map_liquidHeaderTypeFlags::Ocean)) + type = NAV_AREA_WATER; + else if (liquidFlags.HasFlag(map_liquidHeaderTypeFlags::Magma | map_liquidHeaderTypeFlags::Slime)) + type = NAV_AREA_MAGMA_SLIME; + + // indexing is weird... + // after a lot of trial and error, this is what works: + // vertex = y*vertsX+x + // tile = x*tilesY+y + // flag = y*tilesY+x + + uint32 liqOffset = meshData.liquidVerts.size() / 3; + meshData.liquidVerts.resize(meshData.liquidVerts.size() + vertsX * vertsY * 3); + float* liquidVerts = meshData.liquidVerts.data(); + for (uint32 x = 0; x < vertsX; ++x) + { + for (uint32 y = 0; y < vertsY; ++y) + { + G3D::Vector3 vert = G3D::Vector3(corner.x + x * GRID_PART_SIZE, corner.y + y * GRID_PART_SIZE, data[y * vertsX + x]); + vert = vert * rotation * scale + position; + vert.x *= -1.f; + vert.y *= -1.f; + liquidVerts[(liqOffset + x * vertsY + y) * 3 + 0] = vert.y; + liquidVerts[(liqOffset + x * vertsY + y) * 3 + 1] = vert.z; + liquidVerts[(liqOffset + x * vertsY + y) * 3 + 2] = vert.x; + } + } + + std::size_t liquidSquares = 0; + for (uint32 x = 0; x < tilesX; ++x) + { + for (uint32 y = 0; y < tilesY; ++y) + { + if ((flags[x + y * tilesX] & 0x0f) != 0x0f) + { + uint32 square = x * tilesY + y; + int idx1 = square + x; + int idx2 = square + 1 + x; + int idx3 = square + tilesY + 1 + 1 + x; + int idx4 = square + tilesY + 1 + x; + + std::size_t liquidTriOffset = meshData.liquidTris.size(); + meshData.liquidTris.resize(liquidTriOffset + 6); + int* liquidTris = meshData.liquidTris.data() + liquidTriOffset; + + // top triangle + liquidTris[0] = idx2 + liqOffset; + liquidTris[1] = idx1 + liqOffset; + liquidTris[2] = idx3 + liqOffset; + + // bottom triangle + liquidTris[3] = idx3 + liqOffset; + liquidTris[4] = idx1 + liqOffset; + liquidTris[5] = idx4 + liqOffset; + + ++liquidSquares; + } + } + } + + meshData.liquidType.resize(meshData.liquidType.size() + liquidSquares * 2, type); + } + } + } + + /**************************************************************************/ + void TerrainBuilder::transformVertices(std::vector<G3D::Vector3> const& source, std::vector<float>& dest, float scale, G3D::Matrix3 const& rotation, G3D::Vector3 const& position) + { + std::size_t offset = dest.size(); + dest.resize(dest.size() + source.size() * 3); + float* d = dest.data(); + for (std::size_t i = 0; i < source.size(); ++i) + { + // apply tranform, then mirror along the horizontal axes + G3D::Vector3 v(source[i] * rotation * scale + position); + v.x *= -1.f; + v.y *= -1.f; + d[offset + i * 3 + 0] = v.y; + d[offset + i * 3 + 1] = v.z; + d[offset + i * 3 + 2] = v.x; + } + } + + /**************************************************************************/ + void TerrainBuilder::copyIndices(std::vector<VMAP::MeshTriangle> const& source, std::vector<int>& dest, int offset, bool flip) + { + std::size_t destOffset = dest.size(); + dest.resize(dest.size() + source.size() * 3); + int* d = dest.data(); + if (flip) + { + for (VMAP::MeshTriangle const& triangle : source) + { + d[destOffset++] = triangle.idx2 + offset; + d[destOffset++] = triangle.idx1 + offset; + d[destOffset++] = triangle.idx0 + offset; + } + } + else + { + static_assert(sizeof(VMAP::MeshTriangle) == 3 * sizeof(uint32)); + std::ranges::transform(reinterpret_cast<uint32 const*>(source.data()), reinterpret_cast<uint32 const*>(source.data() + source.size()), + dest.data() + destOffset, [&](int src) { return src + offset; }); + } + } + + /**************************************************************************/ + void TerrainBuilder::copyIndices(std::vector<int> const& source, std::vector<int>& dest, int offset) + { + std::size_t destOffset = dest.size(); + dest.resize(destOffset + source.size()); + std::ranges::transform(source, dest.data() + destOffset, [&](int src) { return src + offset; }); + } + + /**************************************************************************/ + void TerrainBuilder::cleanVertices(std::vector<float>& verts, std::vector<int>& tris) + { + std::unordered_map<int, int> vertMap; + vertMap.reserve(tris.size()); + + int* t = tris.data(); + float* v = verts.data(); + + std::vector<float> cleanVerts; + cleanVerts.reserve(verts.size()); + int count = 0; + // collect all the vertex indices from triangle + for (std::size_t i = 0; i < tris.size(); ++i) + { + auto [vertItr, isNew] = vertMap.try_emplace(t[i], count); + if (!isNew) + continue; + + std::ptrdiff_t index = t[i]; + + cleanVerts.insert(cleanVerts.end(), &v[index * 3], &v[index * 3 + 3]); + count++; + } + + verts = std::move(cleanVerts); + + // update triangles to use new indices + for (std::size_t i = 0; i < tris.size(); ++i) + { + auto it = vertMap.find(t[i]); + if (it == vertMap.end()) + continue; + + t[i] = it->second; + } + } + + /**************************************************************************/ + void TerrainBuilder::loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY, MeshData &meshData, std::vector<OffMeshData> const& offMeshConnections) + { + for (OffMeshData const& offMeshConnection : offMeshConnections) + { + if (mapID != offMeshConnection.MapId || tileX != offMeshConnection.TileX || tileY != offMeshConnection.TileY) + continue; + + meshData.offMeshConnections.push_back(offMeshConnection.From[1]); + meshData.offMeshConnections.push_back(offMeshConnection.From[2]); + meshData.offMeshConnections.push_back(offMeshConnection.From[0]); + + meshData.offMeshConnections.push_back(offMeshConnection.To[1]); + meshData.offMeshConnections.push_back(offMeshConnection.To[2]); + meshData.offMeshConnections.push_back(offMeshConnection.To[0]); + + meshData.offMeshConnectionDirs.push_back(offMeshConnection.ConnectionFlags & OFFMESH_CONNECTION_FLAG_BIDIRECTIONAL); + meshData.offMeshConnectionRads.push_back(offMeshConnection.Radius); // agent size equivalent + // can be used same way as polygon flags + meshData.offMeshConnectionsAreas.push_back(offMeshConnection.AreaId); + meshData.offMeshConnectionsFlags.push_back(offMeshConnection.Flags); + } + } +} diff --git a/src/common/mmaps_common/Generator/TerrainBuilder.h b/src/common/mmaps_common/Generator/TerrainBuilder.h new file mode 100644 index 00000000000..a16d1669425 --- /dev/null +++ b/src/common/mmaps_common/Generator/TerrainBuilder.h @@ -0,0 +1,132 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _MMAP_TERRAIN_BUILDER_H +#define _MMAP_TERRAIN_BUILDER_H + +#include "MMapDefines.h" +#include "WorldModel.h" +#include <G3D/Vector3.h> +#include <boost/filesystem/path.hpp> + +namespace VMAP +{ +class VMapManager; +} + +enum class map_liquidHeaderTypeFlags : uint8; + +namespace MMAP +{ + enum Spot + { + TOP = 1, + RIGHT = 2, + LEFT = 3, + BOTTOM = 4, + ENTIRE = 5 + }; + + enum Grid + { + GRID_V8, + GRID_V9 + }; + + static const int V9_SIZE = 129; + static const int V9_SIZE_SQ = V9_SIZE*V9_SIZE; + static const int V8_SIZE = 128; + static const int V8_SIZE_SQ = V8_SIZE*V8_SIZE; + static const float GRID_SIZE = 533.3333f; + static const float GRID_PART_SIZE = GRID_SIZE/V8_SIZE; + + // see contrib/extractor/system.cpp, CONF_use_minHeight + static const float INVALID_MAP_LIQ_HEIGHT = -2000.f; + static const float INVALID_MAP_LIQ_HEIGHT_MAX = 5000.0f; + + // see following files: + // contrib/extractor/system.cpp + // src/game/Map.cpp + + struct MeshData + { + std::vector<float> solidVerts; + std::vector<int> solidTris; + + std::vector<float> liquidVerts; + std::vector<int> liquidTris; + std::vector<uint8> liquidType; + + // offmesh connection data + std::vector<float> offMeshConnections; // [p0y,p0z,p0x,p1y,p1z,p1x] - per connection + std::vector<float> offMeshConnectionRads; + std::vector<unsigned char> offMeshConnectionDirs; + std::vector<unsigned char> offMeshConnectionsAreas; + std::vector<unsigned short> offMeshConnectionsFlags; + }; + + class TC_MMAPS_COMMON_API TerrainBuilder + { + public: + explicit TerrainBuilder(boost::filesystem::path const& inputDirectory, bool skipLiquid); + + void loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager); + bool loadVMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager); + void loadVMapModel(VMAP::WorldModel const* worldModel, G3D::Vector3 const& position, G3D::Matrix3 const& rotation, float scale, + MeshData& meshData, VMAP::VMapManager* vmapManager); + void loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, std::vector<OffMeshData> const& offMeshConnections); + + bool usesLiquids() const { return !m_skipLiquid; } + + // vert and triangle methods + static void transformVertices(std::vector<G3D::Vector3> const& source, std::vector<float>& dest, + float scale, G3D::Matrix3 const& rotation, G3D::Vector3 const& position); + static void copyIndices(std::vector<VMAP::MeshTriangle> const& source, std::vector<int>& dest, int offset, bool flip); + static void copyIndices(std::vector<int> const& source, std::vector<int>& dest, int offset); + static void cleanVertices(std::vector<float>& verts, std::vector<int>& tris); + private: + /// Loads a portion of a map's terrain + bool loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, VMAP::VMapManager* vmapManager, Spot portion); + + /// Sets loop variables for selecting only certain parts of a map's terrain + static void getLoopVars(Spot portion, int& loopStart, int& loopEnd, int& loopInc); + + boost::filesystem::path m_inputDirectory; + + /// Controls whether liquids are loaded + bool m_skipLiquid; + + /// Get the vector coordinate for a specific position + static void getHeightCoord(int index, Grid grid, float xOffset, float yOffset, float* coord, float* v); + + /// Get the triangle's vector indices for a specific position + static void getHeightTriangle(int square, Spot triangle, int* indices, int offset, bool liquid = false); + + /// Determines if the specific position's triangles should be rendered + static bool isHole(int square, uint8 const (&holes)[16][16][8]); + + /// Get the liquid vector coordinate for a specific position + static void getLiquidCoord(int index, int index2, float xOffset, float yOffset, float* coord, float* v); + + /// Get the liquid type for a specific position + static map_liquidHeaderTypeFlags getLiquidType(int square, map_liquidHeaderTypeFlags const (&liquid_type)[16][16]); + }; + + TC_MMAPS_COMMON_API extern std::unique_ptr<VMAP::VMapManager> (*CreateVMapManager)(uint32 mapId); +} + +#endif diff --git a/src/common/mmaps_common/Generator/TileBuilder.cpp b/src/common/mmaps_common/Generator/TileBuilder.cpp new file mode 100644 index 00000000000..31ec90a83da --- /dev/null +++ b/src/common/mmaps_common/Generator/TileBuilder.cpp @@ -0,0 +1,568 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "TileBuilder.h" +#include "IntermediateValues.h" +#include "Log.h" +#include "MMapDefines.h" +#include "Memory.h" +#include "StringFormat.h" +#include "VMapManager.h" +#include <DetourNavMeshBuilder.h> + +namespace +{ + struct Tile + { + Tile() : chf(nullptr), solid(nullptr), cset(nullptr), pmesh(nullptr), dmesh(nullptr) {} + ~Tile() + { + rcFreeCompactHeightfield(chf); + rcFreeContourSet(cset); + rcFreeHeightField(solid); + rcFreePolyMesh(pmesh); + rcFreePolyMeshDetail(dmesh); + } + rcCompactHeightfield* chf; + rcHeightfield* solid; + rcContourSet* cset; + rcPolyMesh* pmesh; + rcPolyMeshDetail* dmesh; + }; +} + +namespace MMAP +{ + struct TileConfig + { + TileConfig(bool bigBaseUnit) + { + // these are WORLD UNIT based metrics + // this are basic unit dimentions + // value have to divide GRID_SIZE(533.3333f) ( aka: 0.5333, 0.2666, 0.3333, 0.1333, etc ) + BASE_UNIT_DIM = bigBaseUnit ? 0.5333333f : 0.2666666f; + + // All are in UNIT metrics! + VERTEX_PER_MAP = int(GRID_SIZE / BASE_UNIT_DIM + 0.5f); + VERTEX_PER_TILE = bigBaseUnit ? 40 : 80; // must divide VERTEX_PER_MAP + TILES_PER_MAP = VERTEX_PER_MAP / VERTEX_PER_TILE; + } + + float BASE_UNIT_DIM; + int VERTEX_PER_MAP; + int VERTEX_PER_TILE; + int TILES_PER_MAP; + }; + + TileBuilder::TileBuilder(boost::filesystem::path const& inputDirectory, boost::filesystem::path const& outputDirectory, + Optional<float> maxWalkableAngle, Optional<float> maxWalkableAngleNotSteep, + bool skipLiquid, bool bigBaseUnit, bool debugOutput, std::vector<OffMeshData> const* offMeshConnections) : + m_outputDirectory(outputDirectory), + m_maxWalkableAngle(maxWalkableAngle), + m_maxWalkableAngleNotSteep(maxWalkableAngleNotSteep), + m_bigBaseUnit(bigBaseUnit), + m_debugOutput(debugOutput), + m_terrainBuilder(inputDirectory, skipLiquid), + m_rcContext(false), + m_offMeshConnections(offMeshConnections) + { + } + + TileBuilder::~TileBuilder() = default; + + /**************************************************************************/ + void TileBuilder::buildTile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh) + { + if (shouldSkipTile(mapID, tileX, tileY)) + { + OnTileDone(); + return; + } + + TC_LOG_INFO("maps.mmapgen", "{} [Map {:04}] Building tile [{:02},{:02}]", GetProgressText(), mapID, tileX, tileY); + + MeshData meshData; + + std::unique_ptr<VMAP::VMapManager> vmapManager = CreateVMapManager(mapID); + + // get heightmap data + m_terrainBuilder.loadMap(mapID, tileX, tileY, meshData, vmapManager.get()); + + // get model data + m_terrainBuilder.loadVMap(mapID, tileX, tileY, meshData, vmapManager.get()); + + // if there is no data, give up now + if (meshData.solidVerts.empty() && meshData.liquidVerts.empty()) + { + OnTileDone(); + return; + } + + // remove unused vertices + TerrainBuilder::cleanVertices(meshData.solidVerts, meshData.solidTris); + TerrainBuilder::cleanVertices(meshData.liquidVerts, meshData.liquidTris); + + if (meshData.liquidVerts.empty() && meshData.solidVerts.empty()) + { + OnTileDone(); + return; + } + + // gather all mesh data for final data check, and bounds calculation + std::vector<float> allVerts(meshData.liquidVerts.size() + meshData.solidVerts.size()); + std::ranges::copy(meshData.liquidVerts, allVerts.begin()); + std::ranges::copy(meshData.solidVerts, allVerts.begin() + std::ssize(meshData.liquidVerts)); + + // get bounds of current tile + float bmin[3], bmax[3]; + getTileBounds(tileX, tileY, allVerts.data(), allVerts.size() / 3, bmin, bmax); + + if (m_offMeshConnections) + m_terrainBuilder.loadOffMeshConnections(mapID, tileX, tileY, meshData, *m_offMeshConnections); + + // build navmesh tile + TileResult tileResult = buildMoveMapTile(mapID, tileX, tileY, meshData, bmin, bmax, navMesh->getParams()); + if (tileResult.data) + saveMoveMapTileToFile(mapID, tileX, tileY, navMesh, tileResult); + + OnTileDone(); + } + + /**************************************************************************/ + TileBuilder::TileResult TileBuilder::buildMoveMapTile(uint32 mapID, uint32 tileX, uint32 tileY, + MeshData& meshData, float (&bmin)[3], float (&bmax)[3], + dtNavMeshParams const* navMeshParams, std::string_view fileNameSuffix) + { + // console output + std::string tileString = Trinity::StringFormat("[Map {:04}] [{:02},{:02}]:", mapID, tileX, tileY); + TC_LOG_INFO("maps.mmapgen", "{} Building movemap tile...", tileString); + + TileResult tileResult; + + IntermediateValues iv; + + float* tVerts = meshData.solidVerts.data(); + int tVertCount = meshData.solidVerts.size() / 3; + int* tTris = meshData.solidTris.data(); + int tTriCount = meshData.solidTris.size() / 3; + + float* lVerts = meshData.liquidVerts.data(); + int lVertCount = meshData.liquidVerts.size() / 3; + int* lTris = meshData.liquidTris.data(); + int lTriCount = meshData.liquidTris.size() / 3; + uint8* lTriFlags = meshData.liquidType.data(); + + const TileConfig tileConfig = TileConfig(m_bigBaseUnit); + int TILES_PER_MAP = tileConfig.TILES_PER_MAP; + float BASE_UNIT_DIM = tileConfig.BASE_UNIT_DIM; + rcConfig config = GetMapSpecificConfig(mapID, bmin, bmax, tileConfig); + + // this sets the dimensions of the heightfield - should maybe happen before border padding + rcCalcGridSize(config.bmin, config.bmax, config.cs, &config.width, &config.height); + + // allocate subregions : tiles + std::unique_ptr<Tile[]> tiles = std::make_unique<Tile[]>(TILES_PER_MAP * TILES_PER_MAP); + + // Initialize per tile config. + rcConfig tileCfg = config; + tileCfg.width = config.tileSize + config.borderSize * 2; + tileCfg.height = config.tileSize + config.borderSize * 2; + + // merge per tile poly and detail meshes + std::unique_ptr<rcPolyMesh*[]> pmmerge = std::make_unique<rcPolyMesh*[]>(TILES_PER_MAP * TILES_PER_MAP); + std::unique_ptr<rcPolyMeshDetail*[]> dmmerge = std::make_unique<rcPolyMeshDetail*[]>(TILES_PER_MAP * TILES_PER_MAP); + int nmerge = 0; + // build all tiles + for (int y = 0; y < TILES_PER_MAP; ++y) + { + for (int x = 0; x < TILES_PER_MAP; ++x) + { + Tile& tile = tiles[x + y * TILES_PER_MAP]; + + // Calculate the per tile bounding box. + tileCfg.bmin[0] = config.bmin[0] + x * float(config.tileSize * config.cs); + tileCfg.bmin[2] = config.bmin[2] + y * float(config.tileSize * config.cs); + tileCfg.bmax[0] = config.bmin[0] + (x + 1) * float(config.tileSize * config.cs); + tileCfg.bmax[2] = config.bmin[2] + (y + 1) * float(config.tileSize * config.cs); + + tileCfg.bmin[0] -= tileCfg.borderSize * tileCfg.cs; + tileCfg.bmin[2] -= tileCfg.borderSize * tileCfg.cs; + tileCfg.bmax[0] += tileCfg.borderSize * tileCfg.cs; + tileCfg.bmax[2] += tileCfg.borderSize * tileCfg.cs; + + // build heightfield + tile.solid = rcAllocHeightfield(); + if (!tile.solid || !rcCreateHeightfield(&m_rcContext, *tile.solid, tileCfg.width, tileCfg.height, tileCfg.bmin, tileCfg.bmax, tileCfg.cs, tileCfg.ch)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building heightfield!", tileString); + continue; + } + + // mark all walkable tiles, both liquids and solids + + /* we want to have triangles with slope less than walkableSlopeAngleNotSteep (<= 55) to have NAV_AREA_GROUND + * and with slope between walkableSlopeAngleNotSteep and walkableSlopeAngle (55 < .. <= 70) to have NAV_AREA_GROUND_STEEP. + * we achieve this using recast API: memset everything to NAV_AREA_GROUND_STEEP, call rcClearUnwalkableTriangles with 70 so + * any area above that will get RC_NULL_AREA (unwalkable), then call rcMarkWalkableTriangles with 55 to set NAV_AREA_GROUND + * on anything below 55 . Players and idle Creatures can use NAV_AREA_GROUND, while Creatures in combat can use NAV_AREA_GROUND_STEEP. + */ + std::unique_ptr<unsigned char[]> triFlags = std::make_unique<unsigned char[]>(tTriCount); + memset(triFlags.get(), NAV_AREA_GROUND_STEEP, tTriCount * sizeof(unsigned char)); + rcClearUnwalkableTriangles(&m_rcContext, tileCfg.walkableSlopeAngle, tVerts, tVertCount, tTris, tTriCount, triFlags.get()); + rcMarkWalkableTriangles(&m_rcContext, tileCfg.walkableSlopeAngleNotSteep, tVerts, tVertCount, tTris, tTriCount, triFlags.get(), NAV_AREA_GROUND); + rcRasterizeTriangles(&m_rcContext, tVerts, tVertCount, tTris, triFlags.get(), tTriCount, *tile.solid, config.walkableClimb); + + rcFilterLowHangingWalkableObstacles(&m_rcContext, config.walkableClimb, *tile.solid); + rcFilterLedgeSpans(&m_rcContext, tileCfg.walkableHeight, tileCfg.walkableClimb, *tile.solid); + rcFilterWalkableLowHeightSpans(&m_rcContext, tileCfg.walkableHeight, *tile.solid); + + // add liquid triangles + rcRasterizeTriangles(&m_rcContext, lVerts, lVertCount, lTris, lTriFlags, lTriCount, *tile.solid, config.walkableClimb); + + // compact heightfield spans + tile.chf = rcAllocCompactHeightfield(); + if (!tile.chf || !rcBuildCompactHeightfield(&m_rcContext, tileCfg.walkableHeight, tileCfg.walkableClimb, *tile.solid, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed compacting heightfield!", tileString); + continue; + } + + // build polymesh intermediates + if (!rcErodeWalkableArea(&m_rcContext, config.walkableRadius, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed eroding area!", tileString); + continue; + } + + if (!rcMedianFilterWalkableArea(&m_rcContext, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed filtering area!", tileString); + continue; + } + + if (!rcBuildDistanceField(&m_rcContext, *tile.chf)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building distance field!", tileString); + continue; + } + + if (!rcBuildRegions(&m_rcContext, *tile.chf, tileCfg.borderSize, tileCfg.minRegionArea, tileCfg.mergeRegionArea)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building regions!", tileString); + continue; + } + + tile.cset = rcAllocContourSet(); + if (!tile.cset || !rcBuildContours(&m_rcContext, *tile.chf, tileCfg.maxSimplificationError, tileCfg.maxEdgeLen, *tile.cset)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building contours!", tileString); + continue; + } + + // build polymesh + tile.pmesh = rcAllocPolyMesh(); + if (!tile.pmesh || !rcBuildPolyMesh(&m_rcContext, *tile.cset, tileCfg.maxVertsPerPoly, *tile.pmesh)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building polymesh!", tileString); + continue; + } + + tile.dmesh = rcAllocPolyMeshDetail(); + if (!tile.dmesh || !rcBuildPolyMeshDetail(&m_rcContext, *tile.pmesh, *tile.chf, tileCfg.detailSampleDist, tileCfg.detailSampleMaxError, *tile.dmesh)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building polymesh detail!", tileString); + continue; + } + + // free those up + // we may want to keep them in the future for debug + // but right now, we don't have the code to merge them + rcFreeHeightField(tile.solid); + tile.solid = nullptr; + rcFreeCompactHeightfield(tile.chf); + tile.chf = nullptr; + rcFreeContourSet(tile.cset); + tile.cset = nullptr; + + pmmerge[nmerge] = tile.pmesh; + dmmerge[nmerge] = tile.dmesh; + nmerge++; + } + } + + iv.polyMesh = rcAllocPolyMesh(); + if (!iv.polyMesh) + { + TC_LOG_ERROR("maps.mmapgen", "{} alloc iv.polyMesh FAILED!", tileString); + return tileResult; + } + rcMergePolyMeshes(&m_rcContext, pmmerge.get(), nmerge, *iv.polyMesh); + + iv.polyMeshDetail = rcAllocPolyMeshDetail(); + if (!iv.polyMeshDetail) + { + TC_LOG_ERROR("maps.mmapgen", "{} alloc m_dmesh FAILED!", tileString); + return tileResult; + } + rcMergePolyMeshDetails(&m_rcContext, dmmerge.get(), nmerge, *iv.polyMeshDetail); + + // free things up + pmmerge = nullptr; + dmmerge = nullptr; + tiles = nullptr; + + // set polygons as walkable + // TODO: special flags for DYNAMIC polygons, ie surfaces that can be turned on and off + for (int i = 0; i < iv.polyMesh->npolys; ++i) + { + if (uint8 area = iv.polyMesh->areas[i] & NAV_AREA_ALL_MASK) + { + if (area >= NAV_AREA_MIN_VALUE) + iv.polyMesh->flags[i] = 1 << (NAV_AREA_MAX_VALUE - area); + else + iv.polyMesh->flags[i] = NAV_GROUND; // TODO: these will be dynamic in future + } + } + + // setup mesh parameters + dtNavMeshCreateParams params = {}; + params.verts = iv.polyMesh->verts; + params.vertCount = iv.polyMesh->nverts; + params.polys = iv.polyMesh->polys; + params.polyAreas = iv.polyMesh->areas; + params.polyFlags = iv.polyMesh->flags; + params.polyCount = iv.polyMesh->npolys; + params.nvp = iv.polyMesh->nvp; + params.detailMeshes = iv.polyMeshDetail->meshes; + params.detailVerts = iv.polyMeshDetail->verts; + params.detailVertsCount = iv.polyMeshDetail->nverts; + params.detailTris = iv.polyMeshDetail->tris; + params.detailTriCount = iv.polyMeshDetail->ntris; + + params.offMeshConVerts = meshData.offMeshConnections.data(); + params.offMeshConCount = meshData.offMeshConnections.size() / 6; + params.offMeshConRad = meshData.offMeshConnectionRads.data(); + params.offMeshConDir = meshData.offMeshConnectionDirs.data(); + params.offMeshConAreas = meshData.offMeshConnectionsAreas.data(); + params.offMeshConFlags = meshData.offMeshConnectionsFlags.data(); + + params.walkableHeight = BASE_UNIT_DIM * config.walkableHeight; // agent height + params.walkableRadius = BASE_UNIT_DIM * config.walkableRadius; // agent radius + params.walkableClimb = BASE_UNIT_DIM * config.walkableClimb; // keep less that walkableHeight (aka agent height)! + params.tileX = (((bmin[0] + bmax[0]) / 2) - navMeshParams->orig[0]) / GRID_SIZE; + params.tileY = (((bmin[2] + bmax[2]) / 2) - navMeshParams->orig[2]) / GRID_SIZE; + rcVcopy(params.bmin, bmin); + rcVcopy(params.bmax, bmax); + params.cs = config.cs; + params.ch = config.ch; + params.tileLayer = 0; + params.buildBvTree = true; + + // will hold final navmesh + unsigned char* navData = nullptr; + + auto debugOutputWriter = Trinity::make_unique_ptr_with_deleter(m_debugOutput ? &iv : nullptr, + [borderSize = static_cast<unsigned short>(config.borderSize), + outputDir = &m_outputDirectory, fileNameSuffix, + mapID, tileX, tileY, &meshData](IntermediateValues* intermediate) + { + // restore padding so that the debug visualization is correct + for (std::ptrdiff_t i = 0; i < intermediate->polyMesh->nverts; ++i) + { + unsigned short* v = &intermediate->polyMesh->verts[i * 3]; + v[0] += borderSize; + v[2] += borderSize; + } + + intermediate->generateObjFile(*outputDir, fileNameSuffix, mapID, tileX, tileY, meshData); + intermediate->writeIV(*outputDir, fileNameSuffix, mapID, tileX, tileY); + }); + + // these values are checked within dtCreateNavMeshData - handle them here + // so we have a clear error message + if (params.nvp > DT_VERTS_PER_POLYGON) + { + TC_LOG_ERROR("maps.mmapgen", "{} Invalid verts-per-polygon value!", tileString); + return tileResult; + } + + if (params.vertCount >= 0xffff) + { + TC_LOG_ERROR("maps.mmapgen", "{} Too many vertices!", tileString); + return tileResult; + } + + if (!params.vertCount || !params.verts) + { + // occurs mostly when adjacent tiles have models + // loaded but those models don't span into this tile + + // message is an annoyance + //TC_LOG_ERROR("maps.mmapgen", "{} No vertices to build tile!", tileString); + return tileResult; + } + + if (!params.polyCount || !params.polys) + { + // we have flat tiles with no actual geometry - don't build those, its useless + // keep in mind that we do output those into debug info + TC_LOG_ERROR("maps.mmapgen", "{} No polygons to build on tile!", tileString); + return tileResult; + } + + if (!params.detailMeshes || !params.detailVerts || !params.detailTris) + { + TC_LOG_ERROR("maps.mmapgen", "{} No detail mesh to build tile!", tileString); + return tileResult; + } + + TC_LOG_DEBUG("maps.mmapgen", "{} Building navmesh tile...", tileString); + if (!dtCreateNavMeshData(¶ms, &navData, &tileResult.size)) + { + TC_LOG_ERROR("maps.mmapgen", "{} Failed building navmesh tile!", tileString); + return tileResult; + } + + tileResult.data.reset(navData); + return tileResult; + } + + void TileBuilder::saveMoveMapTileToFile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh, + TileResult const& tileResult, std::string_view fileNameSuffix) + { + dtTileRef tileRef = 0; + auto navMeshTile = Trinity::make_unique_ptr_with_deleter<dtTileRef*>(nullptr, [navMesh](dtTileRef const* ref) + { + navMesh->removeTile(*ref, nullptr, nullptr); + }); + + if (navMesh) + { + TC_LOG_DEBUG("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: Adding tile to navmesh...", mapID, tileX, tileY); + // DT_TILE_FREE_DATA tells detour to unallocate memory when the tile + // is removed via removeTile() + dtStatus dtResult = navMesh->addTile(tileResult.data.get(), tileResult.size, 0, 0, &tileRef); + if (!tileRef || !dtStatusSucceed(dtResult)) + { + TC_LOG_ERROR("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: Failed adding tile to navmesh!", mapID, tileX, tileY); + return; + } + + navMeshTile.reset(&tileRef); + } + + // file output + std::string fileName = Trinity::StringFormat("{}/mmaps/{:04}_{:02}_{:02}{}.mmtile", m_outputDirectory.generic_string(), mapID, tileX, tileY, fileNameSuffix); + auto file = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(fileName.c_str(), "wb")); + if (!file) + { + TC_LOG_ERROR("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: {}: Failed to open {} for writing!", mapID, tileX, tileY, strerror(errno), fileName); + return; + } + + TC_LOG_DEBUG("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: Writing to file...", mapID, tileX, tileY); + + // write header + MmapTileHeader header; + header.usesLiquids = m_terrainBuilder.usesLiquids(); + header.size = uint32(tileResult.size); + fwrite(&header, sizeof(MmapTileHeader), 1, file.get()); + + // write data + fwrite(tileResult.data.get(), sizeof(unsigned char), tileResult.size, file.get()); + } + + /**************************************************************************/ + void TileBuilder::getTileBounds(uint32 tileX, uint32 tileY, float const* verts, std::size_t vertCount, float* bmin, float* bmax) + { + // this is for elevation + if (verts && vertCount) + rcCalcBounds(verts, int(vertCount), bmin, bmax); + else + { + bmin[1] = FLT_MIN; + bmax[1] = FLT_MAX; + } + + // this is for width and depth + bmax[0] = (32 - int(tileY)) * GRID_SIZE; + bmax[2] = (32 - int(tileX)) * GRID_SIZE; + bmin[0] = bmax[0] - GRID_SIZE; + bmin[2] = bmax[2] - GRID_SIZE; + } + + /**************************************************************************/ + bool TileBuilder::shouldSkipTile(uint32 /*mapID*/, uint32 /*tileX*/, uint32 /*tileY*/) const + { + if (m_debugOutput) + return false; + + return true; + } + + rcConfig TileBuilder::GetMapSpecificConfig(uint32 mapID, float const (&bmin)[3], float const (&bmax)[3], TileConfig const& tileConfig) const + { + rcConfig config { }; + + rcVcopy(config.bmin, bmin); + rcVcopy(config.bmax, bmax); + + config.maxVertsPerPoly = DT_VERTS_PER_POLYGON; + config.cs = tileConfig.BASE_UNIT_DIM; + config.ch = tileConfig.BASE_UNIT_DIM; + // Keeping these 2 slope angles the same reduces a lot the number of polys. + // 55 should be the minimum, maybe 70 is ok (keep in mind blink uses mmaps), 85 is too much for players + config.walkableSlopeAngle = m_maxWalkableAngle.value_or(55.0f); + config.walkableSlopeAngleNotSteep = m_maxWalkableAngleNotSteep.value_or(55.0f); + config.tileSize = tileConfig.VERTEX_PER_TILE; + config.walkableRadius = m_bigBaseUnit ? 1 : 2; + config.borderSize = config.walkableRadius + 3; + config.maxEdgeLen = tileConfig.VERTEX_PER_TILE + 1; // anything bigger than tileSize + config.walkableHeight = m_bigBaseUnit ? 3 : 6; + // a value >= 3|6 allows npcs to walk over some fences + // a value >= 4|8 allows npcs to walk over all fences + config.walkableClimb = m_bigBaseUnit ? 3 : 6; + config.minRegionArea = rcSqr(60); + config.mergeRegionArea = rcSqr(50); + config.maxSimplificationError = 1.8f; // eliminates most jagged edges (tiny polygons) + config.detailSampleDist = config.cs * 16; + config.detailSampleMaxError = config.ch * 1; + + switch (mapID) + { + // Blade's Edge Arena + case 562: + // This allows to walk on the ropes to the pillars + config.walkableRadius = 0; + break; + // Blackfathom Deeps + case 48: + // Reduce the chance to have underground levels + config.ch *= 2; + break; + default: + break; + } + + return config; + } + + std::string TileBuilder::GetProgressText() const + { + return ""; + } +} diff --git a/src/common/mmaps_common/Generator/TileBuilder.h b/src/common/mmaps_common/Generator/TileBuilder.h new file mode 100644 index 00000000000..f3492e51a2e --- /dev/null +++ b/src/common/mmaps_common/Generator/TileBuilder.h @@ -0,0 +1,100 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_TILE_BUILDER_H +#define TRINITYCORE_TILE_BUILDER_H + +#include "Common.h" +#include "Memory.h" +#include "StringFormat.h" +#include "TerrainBuilder.h" +#include <DetourNavMesh.h> +#include <Recast.h> + +namespace MMAP +{ +struct TileConfig; + +using detour_unique_ptr = std::unique_ptr<unsigned char, decltype(Trinity::unique_ptr_deleter<unsigned char*, &::dtFree>())>; + +class TC_MMAPS_COMMON_API TileBuilder +{ +public: + TileBuilder(boost::filesystem::path const& inputDirectory, + boost::filesystem::path const& outputDirectory, + Optional<float> maxWalkableAngle, + Optional<float> maxWalkableAngleNotSteep, + bool skipLiquid, + bool bigBaseUnit, + bool debugOutput, + std::vector<OffMeshData> const* offMeshConnections); + + TileBuilder(TileBuilder const&) = delete; + TileBuilder(TileBuilder&&) = delete; + + TileBuilder& operator=(TileBuilder const&) = delete; + TileBuilder& operator=(TileBuilder&&) = delete; + + virtual ~TileBuilder(); + + void buildTile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh); + + // move map building + struct TileResult + { + detour_unique_ptr data; + int size = 0; + }; + TileResult buildMoveMapTile(uint32 mapID, + uint32 tileX, + uint32 tileY, + MeshData& meshData, + float (&bmin)[3], + float (&bmax)[3], + dtNavMeshParams const* navMeshParams, + std::string_view fileNameSuffix = ""sv); + + void saveMoveMapTileToFile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh, + TileResult const& tileResult, std::string_view fileNameSuffix = ""sv); + + virtual bool shouldSkipTile(uint32 mapID, uint32 tileX, uint32 tileY) const; + + static void getTileBounds(uint32 tileX, uint32 tileY, + float const* verts, std::size_t vertCount, + float* bmin, float* bmax); + + rcConfig GetMapSpecificConfig(uint32 mapID, float const (&bmin)[3], float const (&bmax)[3], TileConfig const& tileConfig) const; + + virtual std::string GetProgressText() const; + + virtual void OnTileDone() { } + +protected: + boost::filesystem::path m_outputDirectory; + Optional<float> m_maxWalkableAngle; + Optional<float> m_maxWalkableAngleNotSteep; + bool m_bigBaseUnit; + bool m_debugOutput; + + TerrainBuilder m_terrainBuilder; + // build performance - not really used for now + rcContext m_rcContext; + std::vector<OffMeshData> const* m_offMeshConnections; +}; +} + +#endif // TRINITYCORE_TILE_BUILDER_H diff --git a/src/common/Collision/Maps/MMapDefines.h b/src/common/mmaps_common/MMapDefines.h index 522112ebe62..522112ebe62 100644 --- a/src/common/Collision/Maps/MMapDefines.h +++ b/src/common/mmaps_common/MMapDefines.h diff --git a/src/common/Collision/Management/MMapManager.cpp b/src/common/mmaps_common/Management/MMapManager.cpp index d0862a49156..d0862a49156 100644 --- a/src/common/Collision/Management/MMapManager.cpp +++ b/src/common/mmaps_common/Management/MMapManager.cpp diff --git a/src/common/Collision/Management/MMapManager.h b/src/common/mmaps_common/Management/MMapManager.h index b48b773555b..87c09d65e6b 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(); |
