feat(Core/mmaps): Add configuration file for mmaps-generator. (#22506)

This commit is contained in:
Anton Popovichenko 2025-10-14 23:56:09 +02:00 committed by GitHub
parent 10d5a3c553
commit f2f31acdcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 15598 additions and 300 deletions

1
deps/CMakeLists.txt vendored
View File

@ -45,4 +45,5 @@ endif()
if (BUILD_TOOLS_MAPS)
add_subdirectory(bzip2)
add_subdirectory(libmpq)
add_subdirectory(fkYAML)
endif()

View File

@ -81,3 +81,7 @@ recastnavigation (Recast is state of the art navigation mesh construction toolse
{fmt} is an open-source formatting library providing a fast and safe alternative to C stdio and C++ iostreams.
https://github.com/fmtlib/fmt
Version: 7.1.3
fkYAML (A C++ header-only YAML library)
https://github.com/fktn-k/fkYAML
Version: 721edb3e1a817e527fd9e1e18a3bea300822522e

17
deps/fkYAML/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,17 @@
#
# This file is part of the AzerothCore 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.
#
add_library(fkYAML INTERFACE)
target_include_directories(fkYAML INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(fkYAML PROPERTIES FOLDER "deps")

14730
deps/fkYAML/fkYAML/node.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -23,9 +23,6 @@
namespace MMAP
{
static char const* const MAP_FILE_NAME_FORMAT = "{}/mmaps/{:03}.mmap";
static char const* const TILE_FILE_NAME_FORMAT = "{}/mmaps/{:03}{:02}{:02}.mmtile";
// ######################## MMapMgr ########################
MMapMgr::~MMapMgr()
{

View File

@ -39,6 +39,9 @@ inline void dtCustomFree(void* ptr)
// move map related classes
namespace MMAP
{
static char const* const MAP_FILE_NAME_FORMAT = "{}/mmaps/{:03}.mmap";
static char const* const TILE_FILE_NAME_FORMAT = "{}/mmaps/{:03}{:02}{:02}.mmtile";
typedef std::unordered_map<uint32, dtTileRef> MMapTileSet;
typedef std::unordered_map<uint32, dtNavMeshQuery*> NavMeshQuerySet;

View File

@ -26,7 +26,40 @@
#define SIZE_OF_GRIDS 533.3333f
#define MMAP_MAGIC 0x4d4d4150 // 'MMAP'
#define MMAP_VERSION 16
#define MMAP_VERSION 17
struct MmapTileRecastConfig
{
float walkableSlopeAngle;
uint8 walkableRadius; // 1
uint8 walkableHeight; // 1
uint8 walkableClimb; // 1
uint8 padding0{0}; // 1 → align next to 4
uint32 vertexPerMapEdge;
uint32 vertexPerTileEdge;
uint32 tilesPerMapEdge;
float baseUnitDim;
float cellSizeHorizontal;
float cellSizeVertical;
float maxSimplificationError;
bool operator==(const MmapTileRecastConfig& b) const {
return walkableSlopeAngle == b.walkableSlopeAngle &&
walkableRadius == b.walkableRadius &&
walkableHeight == b.walkableHeight &&
walkableClimb == b.walkableClimb &&
vertexPerMapEdge == b.vertexPerMapEdge &&
vertexPerTileEdge == b.vertexPerTileEdge &&
tilesPerMapEdge == b.tilesPerMapEdge &&
baseUnitDim == b.baseUnitDim &&
cellSizeHorizontal == b.cellSizeHorizontal &&
cellSizeVertical == b.cellSizeVertical &&
maxSimplificationError == b.maxSimplificationError;
}
};
static_assert(sizeof(MmapTileRecastConfig) == 36, "Unexpected size of MmapTileRecastConfig");
struct MmapTileHeader
{
@ -37,17 +70,20 @@ struct MmapTileHeader
char usesLiquids{true};
char padding[3] {};
MmapTileRecastConfig recastConfig;
MmapTileHeader() : dtVersion(DT_NAVMESH_VERSION) { }
};
// All padding fields must be handled and initialized to ensure mmaps_generator will produce binary-identical *.mmtile files
static_assert(sizeof(MmapTileHeader) == 20, "MmapTileHeader size is not correct, adjust the padding field size");
static_assert(sizeof(MmapTileHeader) == 56, "MmapTileHeader size is not correct, adjust the padding field size");
static_assert(sizeof(MmapTileHeader) == (sizeof(MmapTileHeader::mmapMagic) +
sizeof(MmapTileHeader::dtVersion) +
sizeof(MmapTileHeader::mmapVersion) +
sizeof(MmapTileHeader::size) +
sizeof(MmapTileHeader::usesLiquids) +
sizeof(MmapTileHeader::padding)), "MmapTileHeader has uninitialized padding fields");
sizeof(MmapTileHeader::padding)+
sizeof(MmapTileRecastConfig)), "MmapTileHeader has uninitialized padding fields");
enum NavTerrain
{

View File

@ -29,6 +29,7 @@
#include "GridNotifiers.h"
#include "GridNotifiersImpl.h"
#include "MMapFactory.h"
#include "MMapMgr.h"
#include "Map.h"
#include "PathGenerator.h"
#include "Player.h"
@ -136,6 +137,40 @@ public:
GridCoord const gridCoord = Acore::ComputeGridCoord(player->GetPositionX(), player->GetPositionY());
handler->PSendSysMessage("{}{}{}.mmtile", player->GetMapId(), gridCoord.x_coord, gridCoord.y_coord);
std::string fileName = Acore::StringFormat(MMAP::TILE_FILE_NAME_FORMAT, sConfigMgr->GetOption<std::string>("DataDir", "."), player->GetMapId(), gridCoord.x_coord, gridCoord.y_coord);
FILE* file = fopen(fileName.c_str(), "rb");
if (!file)
{
LOG_DEBUG("maps", "MMAP:loadMap: Could not open mmtile file '{}'", fileName);
return false;
}
// read header
MmapTileHeader fileHeader;
if (fread(&fileHeader, sizeof(MmapTileHeader), 1, file) != 1 || fileHeader.mmapMagic != MMAP_MAGIC)
{
LOG_ERROR("maps", "MMAP:loadMap: Bad header in mmap {:03}{:02}{:02}.mmtile", player->GetMapId(), gridCoord.x_coord, gridCoord.y_coord);
fclose(file);
return false;
}
fclose(file);
handler->PSendSysMessage("Recast config used:");
handler->PSendSysMessage("- walkableSlopeAngle: {}", fileHeader.recastConfig.walkableSlopeAngle);
const float cellHeight = fileHeader.recastConfig.cellSizeVertical;
handler->PSendSysMessage("- walkableHeight: {} ({} units)", fileHeader.recastConfig.walkableHeight * cellHeight, fileHeader.recastConfig.walkableHeight);
handler->PSendSysMessage("- walkableClimb: {} ({} units)", fileHeader.recastConfig.walkableClimb * cellHeight, fileHeader.recastConfig.walkableClimb);
handler->PSendSysMessage("- walkableRadius: {} ({} units)", fileHeader.recastConfig.walkableRadius * cellHeight, fileHeader.recastConfig.walkableRadius);
handler->PSendSysMessage("- maxSimplificationError: {}", fileHeader.recastConfig.maxSimplificationError);
handler->PSendSysMessage("- vertexPerMapEdge: {}", fileHeader.recastConfig.vertexPerMapEdge);
handler->PSendSysMessage("- vertexPerTileEdge: {}", fileHeader.recastConfig.vertexPerTileEdge);
handler->PSendSysMessage("- tilesPerMapEdge: {}", fileHeader.recastConfig.tilesPerMapEdge);
handler->PSendSysMessage("- baseUnitDim: {}", fileHeader.recastConfig.baseUnitDim);
handler->PSendSysMessage("- cellSizeHorizontal: {}", fileHeader.recastConfig.cellSizeHorizontal);
handler->PSendSysMessage("- cellSizeVertical: {}", fileHeader.recastConfig.cellSizeVertical);
handler->PSendSysMessage("gridloc [{}, {}]", gridCoord.x_coord, gridCoord.y_coord);
// calculate navmesh tile location

View File

@ -140,7 +140,8 @@ foreach(TOOL_NAME ${TOOLS_BUILD_LIST})
mpq
zlib
Recast
g3dlib)
g3dlib
fkYAML)
endif()
unset(TOOL_PUBLIC_INCLUDES)
@ -170,4 +171,8 @@ foreach(TOOL_NAME ${TOOLS_BUILD_LIST})
elseif (WIN32)
install(TARGETS ${TOOL_PROJECT_NAME} DESTINATION "${CMAKE_INSTALL_PREFIX}")
endif()
if (${TOOL_PROJECT_NAME} STREQUAL "mmaps_generator")
install(FILES ${SOURCE_TOOL_PATH}/mmaps-config.yaml DESTINATION bin)
endif()
endforeach()

View File

@ -0,0 +1,276 @@
/*
* This file is part of the AzerothCore 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 Affero General Public License as published by the
* Free Software Foundation; either version 3 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 Affero 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 "Config.h"
#include <filesystem>
#include <fkYAML/node.hpp>
#include "PathCommon.h"
#include "TerrainBuilder.h"
namespace MMAP
{
float ComputeBaseUnitDim(int vertexPerMapEdge)
{
return GRID_SIZE / static_cast<float>(vertexPerMapEdge - 1);
}
std::pair<uint32, uint32> MakeTileKey(uint32 x, uint32 y)
{
return {x, y};
}
bool isCurrentDirectory(const std::string& pathStr) {
try {
const std::filesystem::path givenPath = std::filesystem::canonical(std::filesystem::absolute(pathStr));
const std::filesystem::path currentPath = std::filesystem::canonical(std::filesystem::current_path());
return givenPath == currentPath;
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Filesystem error: " << e.what() << "\n";
return false;
}
}
MmapTileRecastConfig ResolvedMeshConfig::toMMAPTileRecastConfig() const {
MmapTileRecastConfig config;
config.walkableSlopeAngle = walkableSlopeAngle;
config.walkableHeight = walkableHeight;
config.walkableClimb = walkableClimb;
config.walkableRadius = walkableRadius;
config.maxSimplificationError = maxSimplificationError;
config.cellSizeHorizontal = cellSizeHorizontal;
config.cellSizeVertical = cellSizeVertical;
config.baseUnitDim = baseUnitDim;
config.vertexPerMapEdge = vertexPerMapEdge;
config.vertexPerTileEdge = vertexPerTileEdge;
config.tilesPerMapEdge = tilesPerMapEdge;
return config;
}
std::optional<Config> Config::FromFile(std::string_view configFile) {
Config config;
if (!config.LoadConfig(configFile))
return std::nullopt;
return config;
}
Config::Config()
{
}
ResolvedMeshConfig Config::GetConfigForTile(uint32 mapID, uint32 tileX, uint32 tileY) const
{
const MapOverride* mapOverride = nullptr;
const TileOverride* tileOverride = nullptr;
// Lookup map and tile overrides
if (auto mapIt = _maps.find(mapID); mapIt != _maps.end())
{
mapOverride = &mapIt->second;
auto tileIt = mapOverride->tileOverrides.find(MakeTileKey(tileY, tileX));
if (tileIt != mapOverride->tileOverrides.end())
tileOverride = &tileIt->second;
}
// Helper lambdas to resolve values in order: tile -> map -> global
auto resolveFloat = [&](auto TileField, auto MapField, float GlobalValue) -> float {
if (tileOverride && TileField(tileOverride)) return *TileField(tileOverride);
if (mapOverride && MapField(mapOverride)) return *MapField(mapOverride);
return GlobalValue;
};
auto resolveInt = [&](auto TileField, auto MapField, int GlobalValue) -> int {
if (tileOverride && TileField(tileOverride)) return *TileField(tileOverride);
if (mapOverride && MapField(mapOverride)) return *MapField(mapOverride);
return GlobalValue;
};
// Resolve vertex settings
int vertexPerMap = resolveInt(
[](const TileOverride*) { return std::optional<int>{}; },
[](const MapOverride* m) { return m->vertexPerMapEdge; },
_global.vertexPerMapEdge
);
int vertexPerTile = resolveInt(
[](const TileOverride*) { return std::optional<int>{}; },
[](const MapOverride* m) { return m->vertexPerTileEdge; },
_global.vertexPerTileEdge
);
ResolvedMeshConfig config;
config.walkableSlopeAngle = resolveFloat(
[](const TileOverride* t) { return t->walkableSlopeAngle; },
[](const MapOverride* m) { return m->walkableSlopeAngle; },
_global.walkableSlopeAngle
);
config.walkableRadius = resolveInt(
[](const TileOverride* t) { return t->walkableRadius; },
[](const MapOverride* m) { return m->walkableRadius; },
_global.walkableRadius
);
config.walkableHeight = resolveInt(
[](const TileOverride* t) { return t->walkableHeight; },
[](const MapOverride* m) { return m->walkableHeight; },
_global.walkableHeight
);
config.walkableClimb = resolveInt(
[](const TileOverride* t) { return t->walkableClimb; },
[](const MapOverride* m) { return m->walkableClimb; },
_global.walkableClimb
);
config.vertexPerMapEdge = vertexPerMap;
config.vertexPerTileEdge = vertexPerTile;
config.baseUnitDim = ComputeBaseUnitDim(vertexPerMap);
config.tilesPerMapEdge = vertexPerMap / vertexPerTile;
config.maxSimplificationError = _global.maxSimplificationError;
config.cellSizeHorizontal = config.baseUnitDim;
config.cellSizeVertical = config.baseUnitDim;
if (mapOverride && mapOverride->cellSizeHorizontal.has_value())
config.cellSizeHorizontal = *mapOverride->cellSizeHorizontal;
if (mapOverride && mapOverride->cellSizeVertical.has_value())
config.cellSizeVertical = *mapOverride->cellSizeVertical;
return config;
}
bool Config::LoadConfig(std::string_view configFile) {
FILE* f = std::fopen(configFile.data(), "r");
if (!f)
return false;
fkyaml::node root = fkyaml::node::deserialize(f);
std::fclose(f);
if (!root.contains("mmapsConfig"))
return false;
fkyaml::node mmapsNode = root["mmapsConfig"];
auto tryFloat = [](const fkyaml::node& n, const char* key, float& out)
{
if (n.contains(key)) out = n[key].get_value<float>();
};
auto tryInt = [](const fkyaml::node& n, const char* key, int& out)
{
if (n.contains(key)) out = n[key].get_value<int>();
};
auto tryBoolean = [](const fkyaml::node& n, const char* key, bool& out)
{
if (n.contains(key)) out = n[key].get_value<bool>();
};
auto tryString = [](const fkyaml::node& n, const char* key, std::string& out)
{
if (n.contains(key)) out = n[key].get_value<std::string>();
};
tryBoolean(mmapsNode, "skipLiquid", _skipLiquid);
tryBoolean(mmapsNode, "skipContinents", _skipContinents);
tryBoolean(mmapsNode, "skipJunkMaps", _skipJunkMaps);
tryBoolean(mmapsNode, "skipBattlegrounds", _skipBattlegrounds);
tryBoolean(mmapsNode, "debugOutput", _debugOutput);
std::string dataDirPath;
tryString(mmapsNode, "dataDir", dataDirPath);
_dataDir = dataDirPath;
mmapsNode = mmapsNode["meshSettings"];
// Global config
tryFloat(mmapsNode, "walkableSlopeAngle", _global.walkableSlopeAngle);
tryInt(mmapsNode, "walkableHeight", _global.walkableHeight);
tryInt(mmapsNode, "walkableClimb", _global.walkableClimb);
tryInt(mmapsNode, "walkableRadius", _global.walkableRadius);
tryInt(mmapsNode, "vertexPerMapEdge", _global.vertexPerMapEdge);
tryInt(mmapsNode, "vertexPerTileEdge", _global.vertexPerTileEdge);
tryFloat(mmapsNode, "maxSimplificationError", _global.maxSimplificationError);
// Map overrides
if (mmapsNode.contains("mapsOverrides"))
{
fkyaml::node maps = mmapsNode["mapsOverrides"];
for (auto const& mapEntry : maps.as_map())
{
uint32 mapId = std::stoi(mapEntry.first.as_str());
MapOverride override;
fkyaml::node mapNode = mapEntry.second;
if (mapNode.contains("walkableSlopeAngle"))
override.walkableSlopeAngle = mapNode["walkableSlopeAngle"].get_value<float>();
if (mapNode.contains("walkableRadius"))
override.walkableRadius = mapNode["walkableRadius"].get_value<int>();
if (mapNode.contains("walkableHeight"))
override.walkableHeight = mapNode["walkableHeight"].get_value<int>();
if (mapNode.contains("walkableClimb"))
override.walkableClimb = mapNode["walkableClimb"].get_value<int>();
if (mapNode.contains("vertexPerMapEdge"))
override.vertexPerMapEdge = mapNode["vertexPerMapEdge"].get_value<int>();
if (mapNode.contains("cellSizeHorizontal"))
override.cellSizeHorizontal = mapNode["cellSizeHorizontal"].get_value<float>();
if (mapNode.contains("cellSizeVertical"))
override.cellSizeVertical = mapNode["cellSizeVertical"].get_value<float>();
// Tile overrides
if (mapNode.contains("tilesOverrides"))
{
fkyaml::node tiles = mapNode["tilesOverrides"];
for (auto const& tileEntry : tiles.as_map())
{
std::string key = tileEntry.first.as_str();
fkyaml::node tileNode = tileEntry.second;
size_t comma = key.find(',');
if (comma == std::string::npos)
continue;
uint32 tileX = static_cast<uint32>(std::stoi(key.substr(0, comma)));
uint32 tileY = static_cast<uint32>(std::stoi(key.substr(comma + 1)));
TileOverride tileOverride;
if (tileNode.contains("walkableSlopeAngle"))
tileOverride.walkableSlopeAngle = tileNode["walkableSlopeAngle"].get_value<float>();
if (tileNode.contains("walkableRadius"))
tileOverride.walkableRadius = tileNode["walkableRadius"].get_value<int>();
if (tileNode.contains("walkableHeight"))
tileOverride.walkableHeight = tileNode["walkableHeight"].get_value<int>();
if (tileNode.contains("walkableClimb"))
tileOverride.walkableClimb = tileNode["walkableClimb"].get_value<int>();
override.tileOverrides[{tileX, tileY}] = std::move(tileOverride);
}
}
_maps[mapId] = std::move(override);
}
}
// Resolve data dir path. Maybe we need to use an executable path instead of the current dir.
if (isCurrentDirectory(_dataDir.string()) && !std::filesystem::exists(MapsPath()))
if (auto execPath = std::filesystem::path(executableDirectoryPath()); std::filesystem::exists(execPath/ "maps"))
_dataDir = execPath;
return true;
}
}

View File

@ -0,0 +1,159 @@
/*
* This file is part of the AzerothCore 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 Affero General Public License as published by the
* Free Software Foundation; either version 3 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 Affero 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 CONFIG_H
#define CONFIG_H
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <boost/program_options/options_description.hpp>
#include "Define.h"
#include "MapDefines.h"
namespace std
{
template <>
struct hash<std::pair<uint32_t, uint32_t>>
{
std::size_t operator()(const std::pair<uint32_t, uint32_t>& p) const noexcept
{
return std::hash<uint64_t>()((static_cast<uint64_t>(p.first) << 32) | p.second);
}
};
}
namespace MMAP
{
struct ResolvedMeshConfig {
float walkableSlopeAngle;
int walkableRadius;
int walkableHeight;
int walkableClimb;
int vertexPerMapEdge;
int vertexPerTileEdge;
int tilesPerMapEdge;
float baseUnitDim;
float cellSizeHorizontal;
float cellSizeVertical;
float maxSimplificationError;
MmapTileRecastConfig toMMAPTileRecastConfig() const;
};
class Config {
public:
static std::optional<Config> FromFile(std::string_view configFile);
~Config() = default;
ResolvedMeshConfig GetConfigForTile(uint32 mapID, uint32 tileX, uint32 tileY) const;
bool ShouldSkipLiquid() const { return _skipLiquid; }
bool ShouldSkipContinents() const { return _skipContinents; }
bool ShouldSkipJunkMaps() const { return _skipJunkMaps; }
bool ShouldSkipBattlegrounds() const { return _skipBattlegrounds; }
bool IsDebugOutputEnabled() const { return _debugOutput; }
std::string VMapsPath() const { return (_dataDir / "vmaps").string(); }
std::string MapsPath() const { return (_dataDir / "maps").string(); }
std::string MMapsPath() const { return (_dataDir / "mmaps").string(); }
std::string DataDirPath() const { return _dataDir.string(); }
private:
explicit Config();
bool LoadConfig(std::string_view configFile);
struct TileOverride {
std::optional<float> walkableSlopeAngle;
std::optional<int> walkableRadius;
std::optional<int> walkableHeight;
std::optional<int> walkableClimb;
};
struct MapOverride {
std::optional<float> walkableSlopeAngle;
std::optional<int> walkableRadius;
std::optional<int> walkableHeight;
std::optional<int> walkableClimb;
std::optional<int> vertexPerMapEdge;
std::optional<int> vertexPerTileEdge;
// The width/depth of each cell in the XZ-plane grid used for voxelization. [Units: world units]
// A smaller value increases navmesh resolution but also memory and CPU usage.
// Default is equal to calculated baseUnitDim.
// Recast reference: https://github.com/recastnavigation/recastnavigation/blob/bd98d84c274ee06842bf51a4088ca82ac71f8c2d/Recast/Include/Recast.h#L231
std::optional<float> cellSizeHorizontal;
// The height of each cell in the Y-axis used for voxelization. [Units: world units]
// Controls how vertical features are represented. Lower values improve accuracy for uneven terrain.
// Default is equal to calculated baseUnitDim.
// Recast reference: https://github.com/recastnavigation/recastnavigation/blob/bd98d84c274ee06842bf51a4088ca82ac71f8c2d/Recast/Include/Recast.h#L234
std::optional<float> cellSizeVertical;
std::unordered_map<std::pair<uint32, uint32>, TileOverride> tileOverrides;
};
struct GlobalConfig {
// Maximum slope angle (in degrees) NPCs can walk on.
// Surfaces steeper than this will be considered unwalkable.
float walkableSlopeAngle = 60.0f;
// Minimum distance (in cell units) around walkable surfaces.
// Helps prevent NPCs from clipping into walls and narrow gaps.
int walkableRadius = 2;
// Minimum ceiling height (in cell units) NPCs need to pass under an obstacle.
// Controls how much vertical clearance is required.
int walkableHeight = 6;
// Maximum height difference (in cell units) NPCs can step up or down.
// Higher values allow walking over fences, ledges, or steps.
int walkableClimb = 6;
// Number of vertices along one edge of the entire map's navmesh grid.
// Higher values increase mesh resolution but also CPU/memory usage.
int vertexPerMapEdge = 2000;
// Number of vertices along one edge of each tile chunk.
// Must divide (vertexPerMapEdge - 1) evenly for seamless tiles.
// A higher vertex count per tile means fewer total tiles,
// reducing runtime work to load, unload, and manage tiles.
int vertexPerTileEdge = 80;
// Tolerance for how much a polygon can deviate from the original geometry when simplified.
// Higher values produce simpler (faster) meshes but can reduce accuracy.
float maxSimplificationError = 1.8f;
};
GlobalConfig _global;
std::unordered_map<uint32, MapOverride> _maps;
bool _skipLiquid;
bool _skipContinents;
bool _skipJunkMaps;
bool _skipBattlegrounds;
bool _debugOutput;
std::filesystem::path _dataDir;
};
}
#endif //CONFIG_H

View File

@ -1,5 +1,8 @@
Generator command line args
--config [file.*] The path the yaml config file
Default: "mmaps-config.yaml"
--threads [#] Max number of threads used by the generator
Default: 3
@ -11,39 +14,6 @@ Generator command line args
--silent [] Make us script friendly. Do not wait for user input
on error or completion.
--bigBaseUnit [true|false] Generate tile/map using bigger basic unit.
Use this option only if you have unexpected gaps.
false: use normal metrics (default)
--maxAngle [#] Max walkable inclination angle
float between 45 and 90 degrees (default 60)
--skipLiquid [true|false] extract liquid data for maps
false: include liquid data (default)
--skipContinents [true|false] continents are maps 0 (Eastern Kingdoms),
1 (Kalimdor), 530 (Outlands), 571 (Northrend)
false: build continents (default)
--skipJunkMaps [true|false] junk maps include some unused
maps, transport maps, and some other
true: skip junk maps (default)
--skipBattlegrounds [true|false] does not include PVP arenas
false: skip battlegrounds (default)
--debugOutput [true|false] create debugging files for use with RecastDemo
if you are only creating mmaps for use with Moongose,
you don't want debugging files
false: don't create debugging files (default)
--tile [#,#] Build the specified tile
seperate number with a comma ','
must specify a map number (see below)
@ -58,9 +28,6 @@ examples:
movement_extractor
builds maps using the default settings (see above for defaults)
movement_extractor --skipContinents true
builds the default maps, except continents
movement_extractor 0
builds all tiles of map 0

View File

@ -16,6 +16,8 @@
*/
#include "IntermediateValues.h"
#include <string>
#include "StringFormat.h"
namespace MMAP
{
@ -28,15 +30,15 @@ namespace MMAP
rcFreePolyMeshDetail(polyMeshDetail);
}
void IntermediateValues::writeIV(uint32 mapID, uint32 tileX, uint32 tileY)
void IntermediateValues::writeIV(const std::string& dataPath, uint32 mapID, uint32 tileX, uint32 tileY)
{
char fileName[255];
char fileName[512];
char tileString[25];
sprintf(tileString, "[%02u,%02u]: ", tileX, tileY);
printf("%sWriting debug output... \r", tileString);
std::string name("meshes/%03u%02i%02i.");
std::string name(dataPath+"/meshes/%03u%02i%02i.");
#define DEBUG_WRITE(fileExtension,data) \
do { \
@ -198,16 +200,19 @@ namespace MMAP
fwrite(mesh->meshes, sizeof(int), mesh->nmeshes * 4, file);
}
void IntermediateValues::generateObjFile(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData)
void IntermediateValues::generateObjFile(const std::string& dataPath, uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData)
{
char objFileName[255];
sprintf(objFileName, "meshes/map%03u%02u%02u.obj", mapID, tileY, tileX);
std::string objFileName = Acore::StringFormat(
"{}/meshes/map{:03}{:02}{:02}.obj",
dataPath,
mapID, tileY, tileX
);
FILE* objFile = fopen(objFileName, "wb");
FILE* objFile = fopen(objFileName.c_str(), "wb");
if (!objFile)
{
char message[1024];
sprintf(message, "Failed to open %s for writing!\n", objFileName);
sprintf(message, "Failed to open %s for writing!\n", objFileName.c_str());
perror(message);
return;
}
@ -237,13 +242,17 @@ namespace MMAP
sprintf(tileString, "[%02u,%02u]: ", tileY, tileX);
printf("%sWriting debug output... \r", tileString);
sprintf(objFileName, "meshes/%03u.map", mapID);
objFileName = Acore::StringFormat(
"{}/meshes/{:03}.map",
dataPath,
mapID
);
objFile = fopen(objFileName, "wb");
objFile = fopen(objFileName.c_str(), "wb");
if (!objFile)
{
char message[1024];
sprintf(message, "Failed to open %s for writing!\n", objFileName);
sprintf(message, "Failed to open %s for writing!\n", objFileName.c_str());
perror(message);
return;
}
@ -252,12 +261,17 @@ namespace MMAP
fwrite(&b, sizeof(char), 1, objFile);
fclose(objFile);
sprintf(objFileName, "meshes/%03u%02u%02u.mesh", mapID, tileY, tileX);
objFile = fopen(objFileName, "wb");
objFileName = Acore::StringFormat(
"{}/meshes/{:03}{:02}{:02}.mesh",
dataPath,
mapID, tileY, tileX
);
objFile = fopen(objFileName.c_str(), "wb");
if (!objFile)
{
char message[1024];
sprintf(message, "Failed to open %s for writing!\n", objFileName);
sprintf(message, "Failed to open %s for writing!\n", objFileName.c_str());
perror(message);
return;
}

View File

@ -35,7 +35,7 @@ namespace MMAP
IntermediateValues() {}
~IntermediateValues();
void writeIV(uint32 mapID, uint32 tileX, uint32 tileY);
void writeIV(const std::string& dataPath, uint32 mapID, uint32 tileX, uint32 tileY);
void debugWrite(FILE* file, const rcHeightfield* mesh);
void debugWrite(FILE* file, const rcCompactHeightfield* chf);
@ -43,7 +43,7 @@ namespace MMAP
void debugWrite(FILE* file, const rcPolyMesh* mesh);
void debugWrite(FILE* file, const rcPolyMeshDetail* mesh);
void generateObjFile(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData);
void generateObjFile(const std::string& dataPath, uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData);
};
}
#endif

View File

@ -16,28 +16,28 @@
*/
#include "MapBuilder.h"
#include <DetourCommon.h>
#include <DetourNavMesh.h>
#include <DetourNavMeshBuilder.h>
#include "IntermediateValues.h"
#include "MapDefines.h"
#include "MapTree.h"
#include "MMapMgr.h"
#include "ModelInstance.h"
#include "PathCommon.h"
#include "StringFormat.h"
#include "VMapMgr2.h"
#include <DetourCommon.h>
#include <DetourNavMesh.h>
#include <DetourNavMeshBuilder.h>
namespace MMAP
{
TileBuilder::TileBuilder(MapBuilder* mapBuilder, bool skipLiquid, bool bigBaseUnit, bool debugOutput) :
m_bigBaseUnit(bigBaseUnit),
TileBuilder::TileBuilder(MapBuilder* mapBuilder, bool skipLiquid, bool debugOutput) :
m_debugOutput(debugOutput),
m_mapBuilder(mapBuilder),
m_terrainBuilder(nullptr),
m_workerThread(&TileBuilder::WorkerThread, this),
m_rcContext(nullptr)
{
m_terrainBuilder = new TerrainBuilder(skipLiquid);
m_terrainBuilder = new TerrainBuilder(m_mapBuilder->getConfig().DataDirPath(), skipLiquid);
m_rcContext = new rcContext(false);
}
@ -55,26 +55,22 @@ namespace MMAP
m_workerThread.join();
}
MapBuilder::MapBuilder(float maxWalkableAngle, bool skipLiquid,
bool skipContinents, bool skipJunkMaps, bool skipBattlegrounds,
bool debugOutput, bool bigBaseUnit, int mapid, const char* offMeshFilePath, unsigned int threads) :
m_debugOutput (debugOutput),
MapBuilder::MapBuilder(Config* config, int mapid, const char* offMeshFilePath, unsigned int threads) :
m_config (config),
m_debugOutput (config->IsDebugOutputEnabled()),
m_offMeshFilePath (offMeshFilePath),
m_threads (threads),
m_skipContinents (skipContinents),
m_skipJunkMaps (skipJunkMaps),
m_skipBattlegrounds (skipBattlegrounds),
m_skipLiquid (skipLiquid),
m_maxWalkableAngle (maxWalkableAngle),
m_bigBaseUnit (bigBaseUnit),
m_skipContinents (config->ShouldSkipContinents()),
m_skipJunkMaps (config->ShouldSkipJunkMaps()),
m_skipBattlegrounds (config->ShouldSkipBattlegrounds()),
m_skipLiquid (config->ShouldSkipLiquid()),
m_mapid (mapid),
m_totalTiles (0u),
m_totalTilesProcessed(0u),
_cancelationToken (false)
{
m_terrainBuilder = new TerrainBuilder(skipLiquid);
m_terrainBuilder = new TerrainBuilder(config->DataDirPath(), config->ShouldSkipLiquid());
m_rcContext = new rcContext(false);
@ -105,7 +101,7 @@ namespace MMAP
char filter[12];
printf("Discovering maps... ");
getDirContents(files, "maps");
getDirContents(files, m_config->MapsPath());
for (auto & file : files)
{
mapID = uint32(atoi(file.substr(0, file.size() - 8).c_str()));
@ -117,7 +113,7 @@ namespace MMAP
}
files.clear();
getDirContents(files, "vmaps", "*.vmtree");
getDirContents(files, m_config->VMapsPath(), "*.vmtree");
for (auto & file : files)
{
mapID = uint32(atoi(file.substr(0, file.size() - 7).c_str()));
@ -138,7 +134,7 @@ namespace MMAP
sprintf(filter, "%03u*.vmtile", mapID);
files.clear();
getDirContents(files, "vmaps", filter);
getDirContents(files, m_config->VMapsPath(), filter);
for (auto & file : files)
{
fsize = file.size();
@ -153,7 +149,7 @@ namespace MMAP
sprintf(filter, "%03u*", mapID);
files.clear();
getDirContents(files, "maps", filter);
getDirContents(files, m_config->MapsPath(), filter);
for (auto & file : files)
{
fsize = file.size();
@ -209,7 +205,7 @@ namespace MMAP
for (unsigned int i = 0; i < m_threads; ++i)
{
m_tileBuilders.push_back(new TileBuilder(this, m_skipLiquid, m_bigBaseUnit, m_debugOutput));
m_tileBuilders.push_back(new TileBuilder(this, m_skipLiquid, m_debugOutput));
}
if (mapID)
@ -367,7 +363,7 @@ namespace MMAP
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 tileBuilder = TileBuilder(this, m_skipLiquid, m_debugOutput);
tileBuilder.buildMoveMapTile(mapId, tileX, tileY, data, bmin, bmax, navMesh);
fclose(file);
}
@ -385,7 +381,7 @@ namespace MMAP
/// @todo: delete the old tile as the user clearly wants to rebuild it
TileBuilder tileBuilder = TileBuilder(this, m_skipLiquid, m_bigBaseUnit, m_debugOutput);
TileBuilder tileBuilder = TileBuilder(this, m_skipLiquid, m_debugOutput);
tileBuilder.buildTile(mapID, tileX, tileY, navMesh);
dtFreeNavMesh(navMesh);
@ -567,15 +563,18 @@ namespace MMAP
return;
}
char fileName[25];
sprintf(fileName, "mmaps/%03u.mmap", mapID);
const std::string fileName = Acore::StringFormat(
MAP_FILE_NAME_FORMAT,
m_config->DataDirPath(),
mapID
);
FILE* file = fopen(fileName, "wb");
FILE* file = fopen(fileName.c_str(), "wb");
if (!file)
{
dtFreeNavMesh(navMesh);
char message[1024];
sprintf(message, "[Map %03i] Failed to open %s for writing!\n", mapID, fileName);
sprintf(message, "[Map %03i] Failed to open %s for writing!\n", mapID, fileName.c_str());
perror(message);
return;
}
@ -608,16 +607,17 @@ namespace MMAP
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);
ResolvedMeshConfig cfg = m_mapBuilder->getConfig().GetConfigForTile(mapID, tileX, tileY);
int tilesPerMap = cfg.tilesPerMapEdge;
float baseUnitDim = cfg.baseUnitDim;
rcConfig config = m_mapBuilder->getRecastConfig(cfg, bmin, bmax);
// 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];
Tile* tiles = new Tile[tilesPerMap * tilesPerMap];
// Initialize per tile config.
rcConfig tileCfg = config;
@ -625,15 +625,16 @@ namespace MMAP
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];
rcPolyMesh** pmmerge = new rcPolyMesh*[tilesPerMap * tilesPerMap];
rcPolyMeshDetail** dmmerge = new rcPolyMeshDetail*[tilesPerMap * tilesPerMap];
int nmerge = 0;
// build all tiles
for (int y = 0; y < TILES_PER_MAP; ++y)
for (int y = 0; y < tilesPerMap; ++y)
{
for (int x = 0; x < TILES_PER_MAP; ++x)
for (int x = 0; x < tilesPerMap; ++x)
{
Tile& tile = tiles[x + y * TILES_PER_MAP];
Tile& tile = tiles[x + y * tilesPerMap];
// Calculate the per tile bounding box.
tileCfg.bmin[0] = config.bmin[0] + x * float(config.tileSize * config.cs);
@ -790,9 +791,9 @@ namespace MMAP
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.walkableHeight = baseUnitDim * config.walkableHeight; // agent height
params.walkableRadius = baseUnitDim * config.walkableRadius; // agent radius
params.walkableClimb = baseUnitDim * 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);
@ -861,13 +862,17 @@ namespace MMAP
}
// file output
char fileName[255];
sprintf(fileName, "mmaps/%03u%02i%02i.mmtile", mapID, tileY, tileX);
FILE* file = fopen(fileName, "wb");
const std::string fileName = Acore::StringFormat(
TILE_FILE_NAME_FORMAT,
m_mapBuilder->getConfig().DataDirPath(),
mapID, tileY, tileX
);
FILE* file = fopen(fileName.c_str(), "wb");
if (!file)
{
char message[1024];
sprintf(message, "[Map %03i] Failed to open %s for writing!\n", mapID, fileName);
sprintf(message, "[Map %03i] Failed to open %s for writing!\n", mapID, fileName.c_str());
perror(message);
navMesh->removeTile(tileRef, nullptr, nullptr);
break;
@ -879,6 +884,7 @@ namespace MMAP
MmapTileHeader header;
header.usesLiquids = m_terrainBuilder->usesLiquids();
header.size = uint32(navDataSize);
header.recastConfig = cfg.toMMAPTileRecastConfig();
fwrite(&header, sizeof(MmapTileHeader), 1, file);
// write data
@ -899,8 +905,8 @@ namespace MMAP
v[2] += (unsigned short)config.borderSize;
}
iv.generateObjFile(mapID, tileX, tileY, meshData);
iv.writeIV(mapID, tileX, tileY);
iv.generateObjFile(m_mapBuilder->getConfig().DataDirPath(), mapID, tileX, tileY, meshData);
iv.writeIV(m_mapBuilder->getConfig().DataDirPath(), mapID, tileX, tileY);
}
}
@ -1028,9 +1034,13 @@ namespace MMAP
/**************************************************************************/
bool TileBuilder::shouldSkipTile(uint32 mapID, uint32 tileX, uint32 tileY) const
{
char fileName[255];
sprintf(fileName, "mmaps/%03u%02i%02i.mmtile", mapID, tileY, tileX);
FILE* file = fopen(fileName, "rb");
const std::string fileName = Acore::StringFormat(
TILE_FILE_NAME_FORMAT,
m_mapBuilder->getConfig().DataDirPath(),
mapID, tileY, tileX
);
FILE* file = fopen(fileName.c_str(), "rb");
if (!file)
return false;
@ -1046,10 +1056,11 @@ namespace MMAP
if (header.mmapVersion != MMAP_VERSION)
return false;
return true;
const auto desiredRecastConfig = m_mapBuilder->getConfig().GetConfigForTile(mapID, tileX, tileY).toMMAPTileRecastConfig();
return header.recastConfig == desiredRecastConfig;
}
rcConfig MapBuilder::GetMapSpecificConfig(uint32 mapID, float bmin[3], float bmax[3], const TileConfig &tileConfig) const
rcConfig MapBuilder::getRecastConfig(const ResolvedMeshConfig &cfg, float bmin[3], float bmax[3]) const
{
rcConfig config;
memset(&config, 0, sizeof(rcConfig));
@ -1058,39 +1069,20 @@ namespace MMAP
rcVcopy(config.bmax, bmax);
config.maxVertsPerPoly = DT_VERTS_PER_POLYGON;
config.cs = tileConfig.BASE_UNIT_DIM;
config.ch = tileConfig.BASE_UNIT_DIM;
config.walkableSlopeAngle = m_maxWalkableAngle;
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.cs = cfg.cellSizeHorizontal;
config.ch = cfg.cellSizeVertical;
config.walkableSlopeAngle = cfg.walkableSlopeAngle;
config.tileSize = cfg.vertexPerTileEdge;
config.walkableRadius = cfg.walkableRadius;
config.borderSize = cfg.walkableRadius + 3;
config.maxEdgeLen = cfg.vertexPerTileEdge + 1; // anything bigger than tileSize
config.walkableHeight = cfg.walkableHeight;
config.walkableClimb = cfg.walkableClimb;
config.minRegionArea = rcSqr(60);
config.mergeRegionArea = rcSqr(50);
config.maxSimplificationError = 1.8f; // eliminates most jagged edges (tiny polygons)
config.maxSimplificationError = cfg.maxSimplificationError; // 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;
}

View File

@ -24,6 +24,7 @@
#include <thread>
#include <vector>
#include "Config.h"
#include "Optional.h"
#include "TerrainBuilder.h"
@ -71,27 +72,6 @@ namespace MMAP
rcPolyMeshDetail* dmesh{nullptr};
};
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;
};
struct TileInfo
{
TileInfo() : m_mapId(uint32(-1)), m_tileX(), m_tileY(), m_navMeshParams() {}
@ -109,7 +89,6 @@ namespace MMAP
public:
TileBuilder(MapBuilder* mapBuilder,
bool skipLiquid,
bool bigBaseUnit,
bool debugOutput);
TileBuilder(TileBuilder&&) = default;
@ -131,7 +110,6 @@ namespace MMAP
bool shouldSkipTile(uint32 mapID, uint32 tileX, uint32 tileY) const;
private:
bool m_bigBaseUnit;
bool m_debugOutput;
MapBuilder* m_mapBuilder;
@ -145,13 +123,7 @@ namespace MMAP
{
friend class TileBuilder;
public:
MapBuilder(float maxWalkableAngle,
bool skipLiquid,
bool skipContinents,
bool skipJunkMaps,
bool skipBattlegrounds,
bool debugOutput,
bool bigBaseUnit,
MapBuilder(Config* config,
int mapid,
char const* offMeshFilePath,
unsigned int threads);
@ -166,6 +138,7 @@ namespace MMAP
// builds list of maps, then builds all of mmap tiles (based on the skip settings)
void buildMaps(Optional<uint32> mapID);
const Config& getConfig() const { return *m_config; }
private:
// builds all mmap tiles for the specified map id (ignores skip settings)
void buildMap(uint32 mapID);
@ -184,7 +157,7 @@ namespace MMAP
bool isTransportMap(uint32 mapID) const;
bool isContinentMap(uint32 mapID) const;
rcConfig GetMapSpecificConfig(uint32 mapID, float bmin[3], float bmax[3], const TileConfig &tileConfig) const;
rcConfig getRecastConfig(const ResolvedMeshConfig &cfg, float bmin[3], float bmax[3]) const;
uint32 percentageDone(uint32 totalTiles, uint32 totalTilesDone) const;
uint32 currentPercentageDone() const;
@ -201,10 +174,10 @@ namespace MMAP
bool m_skipBattlegrounds;
bool m_skipLiquid;
float m_maxWalkableAngle;
bool m_bigBaseUnit;
int32 m_mapid;
Config* m_config;
std::atomic<uint32> m_totalTiles;
std::atomic<uint32> m_totalTilesProcessed;

View File

@ -20,6 +20,7 @@
#include <string>
#include <vector>
#include <boost/dll/runtime_symbol_info.hpp>
#ifndef _WIN32
#include <cstddef>
@ -35,6 +36,11 @@
namespace MMAP
{
inline std::string executableDirectoryPath()
{
return boost::dll::program_location().parent_path().string();
}
inline bool matchWildcardFilter(const char* filter, const char* str)
{
if (!filter || !str)

View File

@ -15,6 +15,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Config.h"
#include "MapBuilder.h"
#include "PathCommon.h"
#include "Timer.h"
@ -23,37 +24,34 @@
using namespace MMAP;
bool checkDirectories(bool debugOutput)
bool checkDirectories(const std::string &dataDirPath, bool debugOutput)
{
std::vector<std::string> dirFiles;
if (getDirContents(dirFiles, "maps") == LISTFILE_DIRECTORY_NOT_FOUND || dirFiles.empty())
if (getDirContents(dirFiles, (std::filesystem::path(dataDirPath) / "maps").string()) == LISTFILE_DIRECTORY_NOT_FOUND || dirFiles.empty())
{
printf("'maps' directory is empty or does not exist\n");
return false;
}
dirFiles.clear();
if (getDirContents(dirFiles, "vmaps", "*.vmtree") == LISTFILE_DIRECTORY_NOT_FOUND || dirFiles.empty())
if (getDirContents(dirFiles, (std::filesystem::path(dataDirPath) / "vmaps").string(), "*.vmtree") == LISTFILE_DIRECTORY_NOT_FOUND || dirFiles.empty())
{
printf("'vmaps' directory is empty or does not exist\n");
return false;
}
dirFiles.clear();
if (getDirContents(dirFiles, "mmaps") == LISTFILE_DIRECTORY_NOT_FOUND)
if (getDirContents(dirFiles, (std::filesystem::path(dataDirPath) / "mmaps").string()) == LISTFILE_DIRECTORY_NOT_FOUND)
{
return boost::filesystem::create_directory("mmaps");
return boost::filesystem::create_directory((std::filesystem::path(dataDirPath) / "mmaps").string());
}
dirFiles.clear();
if (debugOutput)
if (debugOutput && getDirContents(dirFiles, (std::filesystem::path(dataDirPath) / "meshes").string()) == LISTFILE_DIRECTORY_NOT_FOUND)
{
if (getDirContents(dirFiles, "meshes") == LISTFILE_DIRECTORY_NOT_FOUND)
{
printf("'meshes' directory does not exist (no place to put debugOutput files)\n");
return false;
}
printf("'meshes' directory does not exist creating...\n");
return boost::filesystem::create_directory((std::filesystem::path(dataDirPath) / "meshes").string());
}
return true;
@ -63,32 +61,24 @@ bool handleArgs(int argc, char** argv,
int& mapnum,
int& tileX,
int& tileY,
float& maxAngle,
bool& skipLiquid,
bool& skipContinents,
bool& skipJunkMaps,
bool& skipBattlegrounds,
bool& debugOutput,
std::string& configFilePath,
bool& silent,
bool& bigBaseUnit,
char*& offMeshInputPath,
char*& file,
unsigned int& threads)
{
bool hasCustomConfigPath = false;
char* param = nullptr;
for (int i = 1; i < argc; ++i)
{
if (strcmp(argv[i], "--maxAngle") == 0)
if (strcmp(argv[i], "--config") == 0)
{
param = argv[++i];
if (!param)
return false;
float maxangle = atof(param);
if (maxangle <= 90.f && maxangle >= 45.f)
maxAngle = maxangle;
else
printf("invalid option for '--maxAngle', using default\n");
hasCustomConfigPath = true;
configFilePath = param;
}
else if (strcmp(argv[i], "--threads") == 0)
{
@ -126,88 +116,10 @@ bool handleArgs(int argc, char** argv,
return false;
}
}
else if (strcmp(argv[i], "--skipLiquid") == 0)
{
param = argv[++i];
if (!param)
return false;
if (strcmp(param, "true") == 0)
skipLiquid = true;
else if (strcmp(param, "false") == 0)
skipLiquid = false;
else
printf("invalid option for '--skipLiquid', using default\n");
}
else if (strcmp(argv[i], "--skipContinents") == 0)
{
param = argv[++i];
if (!param)
return false;
if (strcmp(param, "true") == 0)
skipContinents = true;
else if (strcmp(param, "false") == 0)
skipContinents = false;
else
printf("invalid option for '--skipContinents', using default\n");
}
else if (strcmp(argv[i], "--skipJunkMaps") == 0)
{
param = argv[++i];
if (!param)
return false;
if (strcmp(param, "true") == 0)
skipJunkMaps = true;
else if (strcmp(param, "false") == 0)
skipJunkMaps = false;
else
printf("invalid option for '--skipJunkMaps', using default\n");
}
else if (strcmp(argv[i], "--skipBattlegrounds") == 0)
{
param = argv[++i];
if (!param)
return false;
if (strcmp(param, "true") == 0)
skipBattlegrounds = true;
else if (strcmp(param, "false") == 0)
skipBattlegrounds = false;
else
printf("invalid option for '--skipBattlegrounds', using default\n");
}
else if (strcmp(argv[i], "--debugOutput") == 0)
{
param = argv[++i];
if (!param)
return false;
if (strcmp(param, "true") == 0)
debugOutput = true;
else if (strcmp(param, "false") == 0)
debugOutput = false;
else
printf("invalid option for '--debugOutput', using default true\n");
}
else if (strcmp(argv[i], "--silent") == 0)
{
silent = true;
}
else if (strcmp(argv[i], "--bigBaseUnit") == 0)
{
param = argv[++i];
if (!param)
return false;
if (strcmp(param, "true") == 0)
bigBaseUnit = true;
else if (strcmp(param, "false") == 0)
bigBaseUnit = false;
else
printf("invalid option for '--bigBaseUnit', using default false\n");
}
else if (strcmp(argv[i], "--offMeshInput") == 0)
{
param = argv[++i];
@ -229,6 +141,23 @@ bool handleArgs(int argc, char** argv,
}
}
if (!hasCustomConfigPath)
{
FILE* f = fopen(configFilePath.c_str(), "r");
if (!f)
{
auto execRelPath = std::filesystem::path(executableDirectoryPath())/configFilePath;
f = fopen(execRelPath.string().c_str(), "r");
if (!f)
{
printf("Failed to load configuration. Ensure that 'mmaps-config.yaml' exists in the current directory or specify its path using the --config option.'\n");
return false;
}
configFilePath = execRelPath.string();
}
fclose(f);
}
return true;
}
@ -244,42 +173,36 @@ int main(int argc, char** argv)
unsigned int threads = std::thread::hardware_concurrency();
int mapnum = -1;
int tileX = -1, tileY = -1;
float maxAngle = 60.0f;
bool skipLiquid = false,
skipContinents = false,
skipJunkMaps = true,
skipBattlegrounds = false,
debugOutput = false,
silent = false,
bigBaseUnit = false;
bool silent = false;
char* offMeshInputPath = nullptr;
char* file = nullptr;
std::string configFilePath = "mmaps-config.yaml";
bool validParam = handleArgs(argc, argv, mapnum,
tileX, tileY, maxAngle,
skipLiquid, skipContinents, skipJunkMaps, skipBattlegrounds,
debugOutput, silent, bigBaseUnit, offMeshInputPath, file, threads);
tileX, tileY, configFilePath, silent, offMeshInputPath, file, threads);
if (!validParam)
return silent ? -1 : finish("You have specified invalid parameters", -1);
if (mapnum == -1 && debugOutput)
auto config = Config::FromFile(configFilePath);
if (!config)
return silent ? -1 : finish("Failed to load configuration. Ensure that 'mmaps-config.yaml' exists in the current directory or specify its path using the --config option.", -1);
if (mapnum == -1 && config->IsDebugOutputEnabled())
{
if (silent)
return -2;
printf("You have specifed debug output, but didn't specify a map to generate.\n");
printf("You have specified debug output, but didn't specify a map to generate.\n");
printf("This will generate debug output for ALL maps.\n");
printf("Are you sure you want to continue? (y/n) ");
if (getchar() != 'y')
return 0;
}
if (!checkDirectories(debugOutput))
if (!checkDirectories(config->DataDirPath(), config->IsDebugOutputEnabled()))
return silent ? -3 : finish("Press ENTER to close...", -3);
MapBuilder builder(maxAngle, skipLiquid, skipContinents, skipJunkMaps,
skipBattlegrounds, debugOutput, bigBaseUnit, mapnum, offMeshInputPath, threads);
MapBuilder builder(&config.value(), mapnum, offMeshInputPath, threads);
uint32 start = getMSTime();
if (file)

View File

@ -25,6 +25,8 @@
#include <vector>
#include <map>
#include "StringFormat.h"
// ******************************************
// Map file format defines
// ******************************************
@ -80,10 +82,17 @@ struct map_liquidHeader
namespace MMAP
{
static char const* const MAP_FILE_NAME_FORMAT = "{}/{:03}{:02}{:02}.map";
uint32 const MAP_VERSION_MAGIC = 9;
TerrainBuilder::TerrainBuilder(bool skipLiquid) : m_skipLiquid (skipLiquid) { }
TerrainBuilder::TerrainBuilder(const std::string &dataDirPath, bool skipLiquid) :
m_skipLiquid (skipLiquid),
m_mapsPath((std::filesystem::path(dataDirPath) / "maps").string()),
m_vmapsPath((std::filesystem::path(dataDirPath) / "vmaps").string())
{
}
TerrainBuilder::~TerrainBuilder() = default;
/**************************************************************************/
@ -134,10 +143,13 @@ namespace MMAP
/**************************************************************************/
bool TerrainBuilder::loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, Spot portion)
{
char mapFileName[255];
sprintf(mapFileName, "maps/%03u%02u%02u.map", mapID, tileY, tileX);
const std::string mapFileName = Acore::StringFormat(
MAP_FILE_NAME_FORMAT,
m_mapsPath,
mapID, tileY, tileX
);
FILE* mapFile = fopen(mapFileName, "rb");
FILE* mapFile = fopen(mapFileName.c_str(), "rb");
if (!mapFile)
return false;
@ -146,7 +158,7 @@ namespace MMAP
fheader.versionMagic != MAP_VERSION_MAGIC)
{
fclose(mapFile);
printf("%s is the wrong version, please extract new .map files\n", mapFileName);
printf("%s is the wrong version, please extract new .map files\n", mapFileName.c_str());
return false;
}
@ -665,7 +677,7 @@ namespace MMAP
bool TerrainBuilder::loadVMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData)
{
IVMapMgr* vmapMgr = new VMapMgr2();
int result = vmapMgr->loadMap("vmaps", mapID, tileX, tileY);
int result = vmapMgr->loadMap(m_vmapsPath.c_str(), mapID, tileX, tileY);
bool retval = false;
do

View File

@ -76,7 +76,7 @@ namespace MMAP
class TerrainBuilder
{
public:
TerrainBuilder(bool skipLiquid);
TerrainBuilder(const std::string &mapsPath, bool skipLiquid);
~TerrainBuilder();
TerrainBuilder(const TerrainBuilder& tb) = delete;
@ -121,6 +121,9 @@ namespace MMAP
/// Get the liquid type for a specific position
uint8 getLiquidType(int square, const uint8 liquid_type[16][16]);
std::string m_mapsPath;
std::string m_vmapsPath;
};
}

View File

@ -0,0 +1,145 @@
mmapsConfig:
skipLiquid: false
skipContinents: false
skipJunkMaps: true
skipBattlegrounds: false
# Path to the directory containing navigation data files.
# This directory should contain the "maps" and "vmaps" folders,
# and is also where the "mmaps" folder will be created or located.
dataDir: "./"
meshSettings:
# Here we have global config for recast navigation.
# It's possible to override these data on map or tile level (see mapsOverrides).
# Maximum slope angle (in degrees) NPCs can walk on.
# Surfaces steeper than this will be considered unwalkable.
walkableSlopeAngle: 60
# --- Cell Size Calculation ---
# Many parameters below are defined in "cell units".
# In RecastDemo, you often work with world units instead of cell units.
# By default, these cell units are converted to world units using the formula:
#
# cellSize = MMAP::GRID_SIZE / (verticesPerMapEdge - 1)
#
# Where:
# MMAP::GRID_SIZE = 533.3333f (the size of one map tile in world units)
# verticesPerMapEdge = number of vertices along one edge of the full map grid
#
# Example:
# If verticesPerMapEdge = 2000, then:
# cellSize ≈ 533.3333 / (2000 - 1) ≈ 0.2667 world units per cell
#
# To convert a value from cell units to world units (e.g., walkableClimb),
# multiply by cellSize. For example, a walkableClimb of 6 corresponds to:
# 6 * 0.2667 ≈ 1.6 world units
# Minimum ceiling height (in cell units) NPCs need to pass under an obstacle.
# Controls how much vertical clearance is required.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
walkableHeight: 6
# Maximum height difference (in cell units) NPCs can step up or down.
# Higher values allow walking over fences, ledges, or steps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
#
# Vanilla WotLK uses 6, which allows creatures to "jump" over fences.
# Classic WotLK uses 4, which forces creatures to walk around fences.
walkableClimb: 6
# Minimum distance (in cell units) around walkable surfaces.
# Helps prevent NPCs from clipping into walls and narrow gaps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
walkableRadius: 2
# Number of vertices along one edge of the entire map's navmesh grid.
# Higher values increase mesh resolution but also CPU/memory usage.
verticesPerMapEdge: 2000
# Number of vertices along one edge of each tile chunk.
# Must divide (vertexPerMapEdge - 1) evenly for seamless tiles.
# A higher vertex count per tile means fewer total tiles,
# reducing runtime work to load, unload, and manage tiles.
verticesPerTileEdge: 80
# Tolerance for how much a polygon can deviate from the original geometry when simplified.
# Higher values produce simpler (faster) meshes but can reduce accuracy.
maxSimplificationError: 1.8
# You can override any global parameter for a specific map by specifying its map ID.
# Inside each map override, you can also override parameters per individual tile,
# identified by a string "tileX,tileY" (coordinates).
#
# Overrides cascade: global settings → map overrides → tile overrides.
# For example:
#
# mapsOverrides:
# "0": # Map ID 0 overrides
# walkableRadius: 5 # Override global climb height for entire map 0
#
# tilesOverrides:
# "50,70": # Tile at coordinates (50,70) on map 0
# walkableSlopeAngle: 70 # Override slope angle locally just here
# walkableClimb: 4 # Also override climb height for this tile only
#
# "51,71":
# walkableClimb: 3 # Override climb height for tile (51,71)
#
# "48,32":
# walkableClimb: 1 # Even smaller climb for tile (48,32)
#
# "1": # Map ID 1 overrides example
# walkableHeight: 8 # Increase clearance for whole map 1
#
# tilesOverrides:
# "100,100":
# maxSimplificationError: 2.5 # Looser mesh simplification for this tile only
#
# "101,101":
# walkableRadius: 1 # Smaller NPC radius here for tight corridors
#
# This approach allows very fine-grained control of navigation mesh parameters
# on a per-map and per-tile basis, optimizing pathfinding quality and performance.
#
# All parameters defined globally are eligible for override.
# Just specify the parameter name and new value in the override section.
mapsOverrides:
"562": # Blade's Edge Arena
walkableRadius: 0 # This allows walking on the ropes to the pillars
"48": # Blackfathom Deeps
cellSizeVertical: 0.5334 # ch*2 = 0.2667 * 2 ≈ 0.5334. Reduce the chance to have underground levels.
"529": # Arathi Basin
tilesOverrides:
"30,29": # Lumber Mill
# Make sure that Fear will not drop players rom cliff -
# https://github.com/azerothcore/azerothcore-wotlk/pull/22462#issuecomment-3067024680
walkableSlopeAngle: 45
# debugOutput generates debug files in the `meshes` directory for use with RecastDemo.
# This is useful for inspecting and debugging mmap generation visually.
#
# My workflow:
# 1. Install RecastDemo. I'm building it from the source of this fork: https://github.com/jackpoz/recastnavigation
# 2. In-game, move your character to the area you want to debug.
# 3. Type `.mmap loc` in chat. This will output:
# - The current tile file name (e.g., `04832.mmtile`)
# - The Recast config values used to generate that tile
# 4. Enable `debugOutput` and regenerate mmaps (preferably just the tile from step 3).
# - To regenerate only one tile, delete it from the `mmaps` folder.
# 5. After generation, you will find debug files in the `meshes` folder, including an OBJ file (e.g., `map0004832.obj`)
# 6. Copy these debug files to the `Meshes` folder used by RecastDemo.
# - RecastDemo expects this folder to be in the same directory as its executable.
# 7. In RecastDemo:
# - Click "Input Mesh" and select the `.obj` file
# - Choose "Solo Mesh" in the Sample selector
# 8. (Optional) Reuse the Recast config values from step 3:
# - `cellSizeHorizontal` → "Cell Size"
# - `walkableSlopeAngle` → "Max Slope"
# - `walkableClimb` → "Max Climb"
# - and so on
# 9. Scroll to the bottom of RecastDemo UI and press "Build" to generate the navigation mesh
debugOutput: false