/* * 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 . */ #include "TileBuilder.h" #include "IntermediateValues.h" #include "Log.h" #include "MMapDefines.h" #include "Memory.h" #include "StringFormat.h" #include "VMapManager.h" #include 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 maxWalkableAngle, Optional maxWalkableAngleNotSteep, bool skipLiquid, bool bigBaseUnit, bool debugOutput, std::vector 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 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 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 tiles = std::make_unique(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 pmmerge = std::make_unique(TILES_PER_MAP * TILES_PER_MAP); std::unique_ptr dmmerge = std::make_unique(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 triFlags = std::make_unique(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(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(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 ""; } }