/*
* 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 "";
}
}