/* * 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 "MapBuilder.h" #include "IntermediateValues.h" #include "MapDefines.h" #include "MapTree.h" #include "MapUtils.h" #include "Memory.h" #include "MMapDefines.h" #include "ModelInstance.h" #include "PathCommon.h" #include "StringConvert.h" #include "StringFormat.h" #include #include #include #include namespace FileExtensions { static boost::filesystem::path tilelist = ".tilelist"; static boost::filesystem::path vmtile = ".vmtile"; } namespace MMAP { TileBuilder::TileBuilder(MapBuilder* mapBuilder, bool skipLiquid, bool bigBaseUnit, bool debugOutput) : m_bigBaseUnit(bigBaseUnit), m_debugOutput(debugOutput), m_mapBuilder(mapBuilder), m_terrainBuilder(nullptr), m_workerThread(&TileBuilder::WorkerThread, this), m_rcContext(nullptr) { m_terrainBuilder = new TerrainBuilder(skipLiquid); m_rcContext = new rcContext(false); } TileBuilder::~TileBuilder() { WaitCompletion(); delete m_terrainBuilder; delete m_rcContext; } void TileBuilder::WaitCompletion() { if (m_workerThread.joinable()) m_workerThread.join(); } MapBuilder::MapBuilder(Optional maxWalkableAngle, Optional maxWalkableAngleNotSteep, bool skipLiquid, bool skipContinents, bool skipJunkMaps, bool skipBattlegrounds, bool debugOutput, bool bigBaseUnit, int mapid, char const* offMeshFilePath, unsigned int threads) : m_terrainBuilder (nullptr), m_debugOutput (debugOutput), m_threads (threads), m_skipContinents (skipContinents), m_skipJunkMaps (skipJunkMaps), m_skipBattlegrounds (skipBattlegrounds), m_skipLiquid (skipLiquid), m_maxWalkableAngle (maxWalkableAngle), m_maxWalkableAngleNotSteep (maxWalkableAngleNotSteep), m_bigBaseUnit (bigBaseUnit), m_mapid (mapid), m_totalTiles (0u), m_totalTilesProcessed(0u), m_rcContext (nullptr), _cancelationToken (false) { m_terrainBuilder = new TerrainBuilder(skipLiquid); m_rcContext = new rcContext(false); // At least 1 thread is needed m_threads = std::max(1u, m_threads); discoverTiles(); ParseOffMeshConnectionsFile(offMeshFilePath); } /**************************************************************************/ MapBuilder::~MapBuilder() { _cancelationToken = true; _queue.Cancel(); for (auto& builder : m_tileBuilders) delete builder; m_tileBuilders.clear(); m_tiles.clear(); delete m_terrainBuilder; delete m_rcContext; } /**************************************************************************/ void MapBuilder::discoverTiles() { boost::filesystem::directory_iterator end; for (auto itr = boost::filesystem::directory_iterator("maps"); itr != end; ++itr) { if (!boost::filesystem::is_regular_file(*itr)) continue; if (itr->path().extension() != FileExtensions::tilelist) continue; Optional mapId = Trinity::StringTo(std::string_view(itr->path().filename().string()).substr(0, 4)); if (!mapId) continue; if (shouldSkipMap(*mapId)) continue; if (auto tileList = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(itr->path().string().c_str(), "rb"))) { u_map_magic mapMagic = { }; uint32 versionMagic = { }; uint32 build; char tilesData[64 * 64] = { }; if (fread(mapMagic.data(), mapMagic.size(), 1, tileList.get()) == 1 && mapMagic == MapMagic && fread(&versionMagic, sizeof(versionMagic), 1, tileList.get()) == 1 && versionMagic == MapVersionMagic && fread(&build, sizeof(build), 1, tileList.get()) == 1 && fread(std::data(tilesData), 64 * 64, 1, tileList.get()) == 1) { Trinity::Containers::FlatSet& tiles = m_tiles[*mapId]; for (uint32 tileX = 0; tileX < 64; ++tileX) for (uint32 tileY = 0; tileY < 64; ++tileY) if (tilesData[tileX * 64 + tileY] == '1') if (tiles.insert(StaticMapTree::packTileID(tileX, tileY)).second) ++m_totalTiles; } } } for (auto itr = boost::filesystem::directory_iterator("vmaps"); itr != end; ++itr) { if (!boost::filesystem::is_directory(*itr)) continue; Optional mapId = Trinity::StringTo(itr->path().filename().string()); if (!mapId) continue; if (shouldSkipMap(*mapId)) continue; Trinity::Containers::FlatSet& tiles = m_tiles[*mapId]; for (auto fileItr = boost::filesystem::directory_iterator(*itr); fileItr != end; ++fileItr) { if (!boost::filesystem::is_regular_file(*fileItr)) continue; if (fileItr->path().extension() != FileExtensions::vmtile) continue; std::string fileName = fileItr->path().filename().string(); uint32 tileX = Trinity::StringTo(std::string_view(fileName).substr(8, 2)).value_or(0); uint32 tileY = Trinity::StringTo(std::string_view(fileName).substr(5, 2)).value_or(0); uint32 tileID = StaticMapTree::packTileID(tileY, tileX); if (tiles.insert(tileID).second) ++m_totalTiles; } } printf("Discovering maps... found %u.\n", uint32(m_tiles.size())); printf("Discovering tiles... found %u.\n\n", m_totalTiles); } /**************************************************************************/ void MapBuilder::ParseOffMeshConnectionsFile(char const* offMeshFilePath) { // no meshfile input given? if (offMeshFilePath == nullptr) return; auto fp = Trinity::make_unique_ptr_with_deleter<&::fclose>(fopen(offMeshFilePath, "rb")); if (!fp) { printf(" loadOffMeshConnections:: input file %s not found!\n", offMeshFilePath); return; } char buf[512] = { }; while (fgets(buf, 512, fp.get())) { OffMeshData offMesh; int32 scanned = sscanf(buf, "%u %u,%u (%f %f %f) (%f %f %f) %f %hhu %hu", &offMesh.MapId, &offMesh.TileX, &offMesh.TileY, &offMesh.From[0], &offMesh.From[1], &offMesh.From[2], &offMesh.To[0], &offMesh.To[1], &offMesh.To[2], &offMesh.Radius, &offMesh.AreaId, &offMesh.Flags); if (scanned < 10) continue; offMesh.Bidirectional = true; if (scanned < 12) offMesh.Flags = NAV_GROUND; if (scanned < 11) offMesh.AreaId = NAV_AREA_GROUND; m_offMeshConnections.push_back(offMesh); } } /**************************************************************************/ std::span MapBuilder::getTileList(uint32 mapID) const { if (Trinity::Containers::FlatSet const* tiles = Trinity::Containers::MapGetValuePtr(m_tiles, mapID)) return *tiles; return { }; } /**************************************************************************/ void TileBuilder::WorkerThread() { while (true) { TileInfo tileInfo; m_mapBuilder->_queue.WaitAndPop(tileInfo); if (m_mapBuilder->_cancelationToken) return; dtNavMesh* navMesh = dtAllocNavMesh(); if (!navMesh->init(&tileInfo.m_navMeshParams)) { printf("[Map %04i] Failed creating navmesh for tile %i,%i !\n", tileInfo.m_mapId, tileInfo.m_tileX, tileInfo.m_tileY); dtFreeNavMesh(navMesh); return; } buildTile(tileInfo.m_mapId, tileInfo.m_tileX, tileInfo.m_tileY, navMesh); dtFreeNavMesh(navMesh); } } void MapBuilder::buildMaps(Optional mapID) { printf("Using %u threads to generate mmaps\n", m_threads); for (unsigned int i = 0; i < m_threads; ++i) { m_tileBuilders.push_back(new TileBuilder(this, m_skipLiquid, m_bigBaseUnit, m_debugOutput)); } if (mapID) { buildMap(*mapID); } else { // Build all maps if no map id has been specified for (auto& [mapId, _] : m_tiles) buildMap(mapId); } while (!_queue.Empty()) { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } _cancelationToken = true; _queue.Cancel(); for (auto& builder : m_tileBuilders) delete builder; m_tileBuilders.clear(); } /**************************************************************************/ void MapBuilder::buildMeshFromFile(char* name) { FILE* file = fopen(name, "rb"); if (!file) return; printf("Building mesh from file\n"); int tileX, tileY, mapId; if (fread(&mapId, sizeof(int), 1, file) != 1) { fclose(file); return; } if (fread(&tileX, sizeof(int), 1, file) != 1) { fclose(file); return; } if (fread(&tileY, sizeof(int), 1, file) != 1) { fclose(file); return; } dtNavMesh* navMesh = nullptr; buildNavMesh(mapId, navMesh); if (!navMesh) { printf("Failed creating navmesh! \n"); fclose(file); return; } uint32 verticesCount, indicesCount; if (fread(&verticesCount, sizeof(uint32), 1, file) != 1) { fclose(file); return; } if (fread(&indicesCount, sizeof(uint32), 1, file) != 1) { fclose(file); return; } float* verts = new float[verticesCount]; if (fread(verts, sizeof(float), verticesCount, file) != verticesCount) { fclose(file); delete[] verts; return; } int* inds = new int[indicesCount]; if (fread(inds, sizeof(int), indicesCount, file) != indicesCount) { fclose(file); delete[] verts; delete[] inds; return; } MeshData data; for (uint32 i = 0; i < verticesCount; ++i) data.solidVerts.append(verts[i]); delete[] verts; for (uint32 i = 0; i < indicesCount; ++i) data.solidTris.append(inds[i]); delete[] inds; TerrainBuilder::cleanVertices(data.solidVerts, data.solidTris); // get bounds of current tile float bmin[3], bmax[3]; getTileBounds(tileX, tileY, data.solidVerts.getCArray(), data.solidVerts.size() / 3, bmin, bmax); // build navmesh tile TileBuilder tileBuilder = TileBuilder(this, m_skipLiquid, m_bigBaseUnit, m_debugOutput); tileBuilder.buildMoveMapTile(mapId, tileX, tileY, data, bmin, bmax, navMesh); fclose(file); } /**************************************************************************/ void MapBuilder::buildSingleTile(uint32 mapID, uint32 tileX, uint32 tileY) { dtNavMesh* navMesh = nullptr; buildNavMesh(mapID, navMesh); if (!navMesh) { printf("Failed creating navmesh! \n"); return; } // ToDo: delete the old tile as the user clearly wants to rebuild it TileBuilder tileBuilder = TileBuilder(this, m_skipLiquid, m_bigBaseUnit, m_debugOutput); tileBuilder.buildTile(mapID, tileX, tileY, navMesh); dtFreeNavMesh(navMesh); _cancelationToken = true; _queue.Cancel(); } /**************************************************************************/ void MapBuilder::buildMap(uint32 mapID) { std::span tiles = getTileList(mapID); if (!tiles.empty()) { // build navMesh dtNavMesh* navMesh = nullptr; buildNavMesh(mapID, navMesh); if (!navMesh) { printf("[Map %04u] Failed creating navmesh!\n", mapID); m_totalTilesProcessed += tiles.size(); return; } // now start building mmtiles for each tile printf("[Map %04u] We have %u tiles. \n", mapID, uint32(tiles.size())); for (uint32 packedTile : tiles) { uint32 tileX, tileY; // unpack tile coords StaticMapTree::unpackTileID(packedTile, tileX, tileY); TileInfo tileInfo; tileInfo.m_mapId = mapID; tileInfo.m_tileX = tileX; tileInfo.m_tileY = tileY; memcpy(&tileInfo.m_navMeshParams, navMesh->getParams(), sizeof(dtNavMeshParams)); _queue.Push(tileInfo); } dtFreeNavMesh(navMesh); } } /**************************************************************************/ void TileBuilder::buildTile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh) { if(shouldSkipTile(mapID, tileX, tileY)) { ++m_mapBuilder->m_totalTilesProcessed; return; } printf("%u%% [Map %04i] Building tile [%02u,%02u]\n", m_mapBuilder->currentPercentageDone(), mapID, tileX, tileY); MeshData meshData; // get heightmap data m_terrainBuilder->loadMap(mapID, tileX, tileY, meshData); // get model data m_terrainBuilder->loadVMap(mapID, tileY, tileX, meshData); // if there is no data, give up now if (!meshData.solidVerts.size() && !meshData.liquidVerts.size()) { ++m_mapBuilder->m_totalTilesProcessed; return; } // remove unused vertices TerrainBuilder::cleanVertices(meshData.solidVerts, meshData.solidTris); TerrainBuilder::cleanVertices(meshData.liquidVerts, meshData.liquidTris); // gather all mesh data for final data check, and bounds calculation G3D::Array allVerts; allVerts.append(meshData.liquidVerts); allVerts.append(meshData.solidVerts); if (!allVerts.size()) { ++m_mapBuilder->m_totalTilesProcessed; return; } // get bounds of current tile float bmin[3], bmax[3]; m_mapBuilder->getTileBounds(tileX, tileY, allVerts.getCArray(), allVerts.size() / 3, bmin, bmax); m_terrainBuilder->loadOffMeshConnections(mapID, tileX, tileY, meshData, m_mapBuilder->m_offMeshConnections); // build navmesh tile buildMoveMapTile(mapID, tileX, tileY, meshData, bmin, bmax, navMesh); ++m_mapBuilder->m_totalTilesProcessed; } /**************************************************************************/ void MapBuilder::buildNavMesh(uint32 mapID, dtNavMesh* &navMesh) { // if map has a parent we use that to generate dtNavMeshParams - worldserver will load all missing tiles from that map int32 navMeshParamsMapId = mapID; int32 parentMapId = sMapStore[mapID].ParentMapID; while (parentMapId != -1) { navMeshParamsMapId = parentMapId; parentMapId = sMapStore[parentMapId].ParentMapID; } std::span tiles = getTileList(navMeshParamsMapId); // old code for non-statically assigned bitmask sizes: ///*** calculate number of bits needed to store tiles & polys ***/ //int tileBits = dtIlog2(dtNextPow2(tiles->size())); //if (tileBits < 1) tileBits = 1; // need at least one bit! //int polyBits = sizeof(dtPolyRef)*8 - SALT_MIN_BITS - tileBits; int polyBits = DT_POLY_BITS; int maxTiles = tiles.size(); int maxPolysPerTile = 1 << polyBits; /*** calculate bounds of map ***/ uint32 tileXMin = 64, tileYMin = 64, tileXMax = 0, tileYMax = 0, tileX, tileY; for (uint32 packedTile : tiles) { StaticMapTree::unpackTileID(packedTile, tileX, tileY); if (tileX > tileXMax) tileXMax = tileX; else if (tileX < tileXMin) tileXMin = tileX; if (tileY > tileYMax) tileYMax = tileY; else if (tileY < tileYMin) tileYMin = tileY; } // use Max because '32 - tileX' is negative for values over 32 float bmin[3], bmax[3]; getTileBounds(tileXMax, tileYMax, nullptr, 0, bmin, bmax); /*** now create the navmesh ***/ // navmesh creation params dtNavMeshParams navMeshParams; memset(&navMeshParams, 0, sizeof(dtNavMeshParams)); navMeshParams.tileWidth = GRID_SIZE; navMeshParams.tileHeight = GRID_SIZE; rcVcopy(navMeshParams.orig, bmin); navMeshParams.maxTiles = maxTiles; navMeshParams.maxPolys = maxPolysPerTile; navMesh = dtAllocNavMesh(); printf("[Map %04u] Creating navMesh...\n", mapID); if (!navMesh->init(&navMeshParams)) { printf("[Map %04u] Failed creating navmesh! \n", mapID); return; } std::string fileName = Trinity::StringFormat("mmaps/{:04}.mmap", mapID); FILE* file = fopen(fileName.c_str(), "wb"); if (!file) { dtFreeNavMesh(navMesh); navMesh = nullptr; perror(Trinity::StringFormat("[Map {:04}] Failed to open {} for writing!\n", mapID, fileName).c_str()); return; } // now that we know navMesh params are valid, we can write them to file fwrite(&navMeshParams, sizeof(dtNavMeshParams), 1, file); fclose(file); } /**************************************************************************/ void TileBuilder::buildMoveMapTile(uint32 mapID, uint32 tileX, uint32 tileY, MeshData &meshData, float bmin[3], float bmax[3], dtNavMesh* navMesh) { // console output std::string tileString = Trinity::StringFormat("[Map {:04}] [{:02},{:02}]: ", mapID, tileX, tileY); printf("%s Building movemap tiles...\n", tileString.c_str()); IntermediateValues iv; float* tVerts = meshData.solidVerts.getCArray(); int tVertCount = meshData.solidVerts.size() / 3; int* tTris = meshData.solidTris.getCArray(); int tTriCount = meshData.solidTris.size() / 3; float* lVerts = meshData.liquidVerts.getCArray(); int lVertCount = meshData.liquidVerts.size() / 3; int* lTris = meshData.liquidTris.getCArray(); int lTriCount = meshData.liquidTris.size() / 3; uint8* lTriFlags = meshData.liquidType.getCArray(); const TileConfig tileConfig = TileConfig(m_bigBaseUnit); int TILES_PER_MAP = tileConfig.TILES_PER_MAP; float BASE_UNIT_DIM = tileConfig.BASE_UNIT_DIM; rcConfig config = m_mapBuilder->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 Tile* tiles = new 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 rcPolyMesh** pmmerge = new rcPolyMesh*[TILES_PER_MAP * TILES_PER_MAP]; rcPolyMeshDetail** dmmerge = new 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)) { printf("%s Failed building heightfield! \n", tileString.c_str()); 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. */ unsigned char* triFlags = new unsigned char[tTriCount]; memset(triFlags, NAV_AREA_GROUND_STEEP, tTriCount*sizeof(unsigned char)); rcClearUnwalkableTriangles(m_rcContext, tileCfg.walkableSlopeAngle, tVerts, tVertCount, tTris, tTriCount, triFlags); rcMarkWalkableTriangles(m_rcContext, tileCfg.walkableSlopeAngleNotSteep, tVerts, tVertCount, tTris, tTriCount, triFlags, NAV_AREA_GROUND); rcRasterizeTriangles(m_rcContext, tVerts, tVertCount, tTris, triFlags, tTriCount, *tile.solid, config.walkableClimb); delete[] triFlags; 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)) { printf("%s Failed compacting heightfield! \n", tileString.c_str()); continue; } // build polymesh intermediates if (!rcErodeWalkableArea(m_rcContext, config.walkableRadius, *tile.chf)) { printf("%s Failed eroding area! \n", tileString.c_str()); continue; } if (!rcMedianFilterWalkableArea(m_rcContext, *tile.chf)) { printf("%s Failed filtering area! \n", tileString.c_str()); continue; } if (!rcBuildDistanceField(m_rcContext, *tile.chf)) { printf("%s Failed building distance field! \n", tileString.c_str()); continue; } if (!rcBuildRegions(m_rcContext, *tile.chf, tileCfg.borderSize, tileCfg.minRegionArea, tileCfg.mergeRegionArea)) { printf("%s Failed building regions! \n", tileString.c_str()); continue; } tile.cset = rcAllocContourSet(); if (!tile.cset || !rcBuildContours(m_rcContext, *tile.chf, tileCfg.maxSimplificationError, tileCfg.maxEdgeLen, *tile.cset)) { printf("%s Failed building contours! \n", tileString.c_str()); continue; } // build polymesh tile.pmesh = rcAllocPolyMesh(); if (!tile.pmesh || !rcBuildPolyMesh(m_rcContext, *tile.cset, tileCfg.maxVertsPerPoly, *tile.pmesh)) { printf("%s Failed building polymesh! \n", tileString.c_str()); continue; } tile.dmesh = rcAllocPolyMeshDetail(); if (!tile.dmesh || !rcBuildPolyMeshDetail(m_rcContext, *tile.pmesh, *tile.chf, tileCfg.detailSampleDist, tileCfg.detailSampleMaxError, *tile.dmesh)) { printf("%s Failed building polymesh detail! \n", tileString.c_str()); 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) { printf("%s alloc iv.polyMesh FAILED!\n", tileString.c_str()); delete[] pmmerge; delete[] dmmerge; delete[] tiles; return; } rcMergePolyMeshes(m_rcContext, pmmerge, nmerge, *iv.polyMesh); iv.polyMeshDetail = rcAllocPolyMeshDetail(); if (!iv.polyMeshDetail) { printf("%s alloc m_dmesh FAILED!\n", tileString.c_str()); delete[] pmmerge; delete[] dmmerge; delete[] tiles; return; } rcMergePolyMeshDetails(m_rcContext, dmmerge, nmerge, *iv.polyMeshDetail); // free things up delete[] pmmerge; delete[] dmmerge; delete[] tiles; // 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; memset(¶ms, 0, sizeof(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.getCArray(); params.offMeshConCount = meshData.offMeshConnections.size()/6; params.offMeshConRad = meshData.offMeshConnectionRads.getCArray(); params.offMeshConDir = meshData.offMeshConnectionDirs.getCArray(); params.offMeshConAreas = meshData.offMeshConnectionsAreas.getCArray(); params.offMeshConFlags = meshData.offMeshConnectionsFlags.getCArray(); 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) - navMesh->getParams()->orig[0]) / GRID_SIZE; params.tileY = (((bmin[2] + bmax[2]) / 2) - navMesh->getParams()->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; int navDataSize = 0; do { // these values are checked within dtCreateNavMeshData - handle them here // so we have a clear error message if (params.nvp > DT_VERTS_PER_POLYGON) { printf("%s Invalid verts-per-polygon value! \n", tileString.c_str()); break; } if (params.vertCount >= 0xffff) { printf("%s Too many vertices! \n", tileString.c_str()); break; } 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 //printf("%sNo vertices to build tile! \n", tileString.c_str()); break; } 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 printf("%s No polygons to build on tile! \n", tileString.c_str()); break; } if (!params.detailMeshes || !params.detailVerts || !params.detailTris) { printf("%s No detail mesh to build tile! \n", tileString.c_str()); break; } printf("%s Building navmesh tile...\n", tileString.c_str()); if (!dtCreateNavMeshData(¶ms, &navData, &navDataSize)) { printf("%s Failed building navmesh tile! \n", tileString.c_str()); break; } dtTileRef tileRef = 0; printf("%s Adding tile to navmesh...\n", tileString.c_str()); // DT_TILE_FREE_DATA tells detour to unallocate memory when the tile // is removed via removeTile() dtStatus dtResult = navMesh->addTile(navData, navDataSize, DT_TILE_FREE_DATA, 0, &tileRef); if (!tileRef || dtResult != DT_SUCCESS) { printf("%s Failed adding tile to navmesh! \n", tileString.c_str()); break; } // file output std::string fileName = Trinity::StringFormat("mmaps/{:04}{:02}{:02}.mmtile", mapID, tileY, tileX); FILE* file = fopen(fileName.c_str(), "wb"); if (!file) { perror(Trinity::StringFormat("[Map {:04}] Failed to open {} for writing!\n", mapID, fileName).c_str()); navMesh->removeTile(tileRef, nullptr, nullptr); break; } printf("%s Writing to file...\n", tileString.c_str()); // write header MmapTileHeader header; header.usesLiquids = m_terrainBuilder->usesLiquids(); header.size = uint32(navDataSize); fwrite(&header, sizeof(MmapTileHeader), 1, file); /* dtMeshHeader* navDataHeader = (dtMeshHeader*)navData; printf("Poly count: %d\n", navDataHeader->polyCount); */ // write data fwrite(navData, sizeof(unsigned char), navDataSize, file); fclose(file); // now that tile is written to disk, we can unload it navMesh->removeTile(tileRef, nullptr, nullptr); } while (false); if (m_debugOutput) { // restore padding so that the debug visualization is correct for (int i = 0; i < iv.polyMesh->nverts; ++i) { unsigned short* v = &iv.polyMesh->verts[i*3]; v[0] += (unsigned short)config.borderSize; v[2] += (unsigned short)config.borderSize; } iv.generateObjFile(mapID, tileX, tileY, meshData); iv.writeIV(mapID, tileX, tileY); } } /**************************************************************************/ void MapBuilder::getTileBounds(uint32 tileX, uint32 tileY, float* verts, int vertCount, float* bmin, float* bmax) const { // this is for elevation if (verts && vertCount) rcCalcBounds(verts, vertCount, bmin, bmax); else { bmin[1] = FLT_MIN; bmax[1] = FLT_MAX; } // this is for width and depth bmax[0] = (32 - int(tileX)) * GRID_SIZE; bmax[2] = (32 - int(tileY)) * GRID_SIZE; bmin[0] = bmax[0] - GRID_SIZE; bmin[2] = bmax[2] - GRID_SIZE; } /**************************************************************************/ bool MapBuilder::shouldSkipMap(uint32 mapID) const { if (m_mapid >= 0) return static_cast(m_mapid) != mapID; if (m_skipContinents) if (isContinentMap(mapID)) return true; if (m_skipJunkMaps) { if (isDevMap(mapID)) return true; if (isTransportMap(mapID)) return true; } if (m_skipBattlegrounds) { if (isBattlegroundMap(mapID)) return true; } return false; } /**************************************************************************/ bool MapBuilder::isTransportMap(uint32 mapID) const { if (MapEntry const* map = Trinity::Containers::MapGetValuePtr(sMapStore, mapID)) return map->MapType == 3; return false; } bool MapBuilder::isDevMap(uint32 mapID) const { if (MapEntry const* map = Trinity::Containers::MapGetValuePtr(sMapStore, mapID)) return (map->Flags & 0x2) != 0; return false; } bool MapBuilder::isBattlegroundMap(uint32 mapID) const { if (MapEntry const* map = Trinity::Containers::MapGetValuePtr(sMapStore, mapID)) return map->InstanceType == 3; return false; } bool MapBuilder::isContinentMap(uint32 mapID) const { switch (mapID) { case 0: case 1: case 530: case 571: case 870: case 1116: case 1220: case 1642: case 1643: case 2222: return true; default: return false; } } /**************************************************************************/ bool TileBuilder::shouldSkipTile(uint32 mapID, uint32 tileX, uint32 tileY) const { std::string fileName = Trinity::StringFormat("mmaps/{:04}{:02}{:02}.mmtile", mapID, tileY, tileX); FILE* file = fopen(fileName.c_str(), "rb"); if (!file) return false; MmapTileHeader header; int count = fread(&header, sizeof(MmapTileHeader), 1, file); fclose(file); if (count != 1) return false; if (header.mmapMagic != MMAP_MAGIC || header.dtVersion != uint32(DT_NAVMESH_VERSION)) return false; if (header.mmapVersion != MMAP_VERSION) return false; return true; } rcConfig MapBuilder::GetMapSpecificConfig(uint32 mapID, float bmin[3], float bmax[3], const TileConfig &tileConfig) const { rcConfig config; memset(&config, 0, sizeof(rcConfig)); 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 ? *m_maxWalkableAngle : 55; config.walkableSlopeAngleNotSteep = m_maxWalkableAngleNotSteep ? *m_maxWalkableAngleNotSteep : 55; 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; } /**************************************************************************/ uint32 MapBuilder::percentageDone(uint32 totalTiles, uint32 totalTilesBuilt) const { if (totalTiles) return totalTilesBuilt * 100 / totalTiles; return 0; } uint32 MapBuilder::currentPercentageDone() const { return percentageDone(m_totalTiles, m_totalTilesProcessed); } }