feat(Core): mmaps config overrides + bot steep-slope-aware nav filter

mmaps extraction (mmaps-config.yaml + MapBuilder):
- Per-map/tile overrides for vertexPerTileEdge and maxSimplificationError; singular vertex*Edge keys.
- Tag 50-60deg slopes NAV_GROUND_STEEP (modAlmostUnwalkableTriangles) on top of clearing >60deg.

Bot nav filter (PathGenerator, MOD_PLAYERBOTS):
- CreateFilter detects a bot via GetSession()->IsBot() and applies a stricter filter: include
  ground/water, exclude lava/slime and NAV_GROUND_STEEP, and cost deep water. It runs in the
  constructor so all bot movement is covered; real players and creatures are unchanged.
- Add NAV_GROUND_STEEP (0x10) nav flag.
- Double MAX_PATH_LENGTH to 148 for long bot routes; expose SetNavTerrainCost/SetExcludeFlags.
This commit is contained in:
bash
2026-06-26 23:10:18 +02:00
parent b39b9a5a46
commit 65018acd79
7 changed files with 111 additions and 21 deletions

View File

@@ -26,7 +26,7 @@
#define SIZE_OF_GRIDS 533.3333f
#define MMAP_MAGIC 0x4d4d4150 // 'MMAP'
#define MMAP_VERSION 19
#define MMAP_VERSION 20
struct MmapTileRecastConfig
{
@@ -87,15 +87,15 @@ static_assert(sizeof(MmapTileHeader) == (sizeof(MmapTileHeader::mmapMagic) +
enum NavTerrain
{
NAV_EMPTY = 0x00,
NAV_GROUND = 0x01,
NAV_MAGMA = 0x02,
NAV_SLIME = 0x04,
NAV_WATER = 0x08,
NAV_UNUSED1 = 0x10,
NAV_UNUSED2 = 0x20,
NAV_UNUSED3 = 0x40,
NAV_UNUSED4 = 0x80
NAV_EMPTY = 0x00,
NAV_GROUND = 0x01,
NAV_MAGMA = 0x02,
NAV_SLIME = 0x04,
NAV_WATER = 0x08,
NAV_GROUND_STEEP = 0x10,
NAV_UNUSED2 = 0x20,
NAV_UNUSED3 = 0x40,
NAV_UNUSED4 = 0x80
// we only have 8 bits
};

View File

@@ -23,6 +23,10 @@
#include "MMapMgr.h"
#include "Map.h"
#include "Metric.h"
#ifdef MOD_PLAYERBOTS
#include "Player.h"
#include "WorldSession.h"
#endif
////////////////// PathGenerator //////////////////
PathGenerator::PathGenerator(WorldObject const* owner) :
@@ -648,12 +652,15 @@ void PathGenerator::CreateFilter()
{
uint16 includeFlags = 0;
uint16 excludeFlags = 0;
#ifdef MOD_PLAYERBOTS
bool isBot = false;
#endif
if (_source->IsCreature())
{
Creature* creature = (Creature*)_source;
if (creature->CanWalk())
includeFlags |= NAV_GROUND; // walk
includeFlags |= (NAV_GROUND | NAV_GROUND_STEEP);
// creatures don't take environmental damage
if (creature->CanEnterWater())
@@ -661,13 +668,36 @@ void PathGenerator::CreateFilter()
}
else // assume Player
{
// perfect support not possible, just stay 'safe'
includeFlags |= (NAV_GROUND | NAV_WATER | NAV_MAGMA);
#ifdef MOD_PLAYERBOTS
// Bots navigate with a stricter filter: include ground + water but exclude lava/slime and
// NAV_GROUND_STEEP (the 50-60deg slopes the extractor tags via modAlmostUnwalkableTriangles), so
// they keep off steep mountainsides and follow gentle ground/roads. Real players are unchanged and
// may still path across steep terrain.
Player const* player = _source->ToPlayer();
if (player && player->GetSession() && player->GetSession()->IsBot())
{
includeFlags |= (NAV_GROUND | NAV_WATER);
excludeFlags |= (NAV_MAGMA | NAV_SLIME | NAV_GROUND_STEEP);
isBot = true;
}
else
#endif
{
// perfect support not possible, just stay 'safe'
includeFlags |= (NAV_GROUND | NAV_GROUND_STEEP | NAV_WATER | NAV_MAGMA);
}
}
_filter.setIncludeFlags(includeFlags);
_filter.setExcludeFlags(excludeFlags);
#ifdef MOD_PLAYERBOTS
// Bots bias their routes away from deep water (swim only when necessary). poly.area == poly.flags ==
// NavTerrain, so NAV_WATER doubles as the water area index. Real players and creatures assign no cost.
if (isBot)
_filter.setAreaCost(NAV_WATER, 20.0f);
#endif
UpdateFilter();
}

View File

@@ -32,8 +32,16 @@ class WorldObject;
// 74*4.0f=296y number_of_points*interval = max_path_len
// this is way more than actual evade range
// I think we can safely cut those down even more
#ifdef MOD_PLAYERBOTS
// Bots travel long-distance to quests; the default 74-poly cap forces
// repeated re-pathfinding mid-route and produces partial paths short of
// the destination. 148 covers most quest movements end-to-end.
#define MAX_PATH_LENGTH 148
#define MAX_POINT_PATH_LENGTH 148
#else
#define MAX_PATH_LENGTH 74
#define MAX_POINT_PATH_LENGTH 74
#endif
#define SMOOTH_PATH_STEP_SIZE 4.0f
#define SMOOTH_PATH_SLOP 0.3f
@@ -81,6 +89,16 @@ class PathGenerator
void SetUseStraightPath(bool useStraightPath) { _useStraightPath = useStraightPath; }
void SetPathLengthLimit(float distance) { _pointPathLimit = std::min<uint32>(uint32(distance/SMOOTH_PATH_STEP_SIZE), MAX_POINT_PATH_LENGTH); }
void SetUseRaycast(bool useRaycast) { _useRaycast = useRaycast; }
// Adjust per-terrain Detour traversal cost on the active query
// filter. Persists across CalculatePath calls until overwritten.
void SetNavTerrainCost(NavTerrain terrain, float cost)
{
_filter.setAreaCost(static_cast<uint8>(terrain), cost);
}
// Replace the active filter's exclude bitmask. Caller may pass
// a single NavTerrain or an OR'd combination (NavTerrain values
// implicitly convert through uint16).
void SetExcludeFlags(uint16 flags) { _filter.setExcludeFlags(flags); }
// result getters
[[nodiscard]] G3D::Vector3 const& GetStartPosition() const { return _startPosition; }

View File

@@ -142,7 +142,11 @@ namespace MMAP
config.vertexPerTileEdge = vertexPerTile;
config.baseUnitDim = ComputeBaseUnitDim(vertexPerMap);
config.tilesPerMapEdge = vertexPerMap / vertexPerTile;
config.maxSimplificationError = _global.maxSimplificationError;
config.maxSimplificationError = resolveFloat(
[](const TileOverride* t) { return t->maxSimplificationError; },
[](const MapOverride* m) { return m->maxSimplificationError; },
_global.maxSimplificationError
);
config.cellSizeHorizontal = config.baseUnitDim;
config.cellSizeVertical = config.baseUnitDim;
@@ -227,10 +231,14 @@ namespace MMAP
override.walkableClimb = mapNode["walkableClimb"].get_value<int>();
if (mapNode.contains("vertexPerMapEdge"))
override.vertexPerMapEdge = mapNode["vertexPerMapEdge"].get_value<int>();
if (mapNode.contains("vertexPerTileEdge"))
override.vertexPerTileEdge = mapNode["vertexPerTileEdge"].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>();
if (mapNode.contains("maxSimplificationError"))
override.maxSimplificationError = mapNode["maxSimplificationError"].get_value<float>();
// Tile overrides
if (mapNode.contains("tilesOverrides"))
@@ -257,6 +265,8 @@ namespace MMAP
tileOverride.walkableHeight = tileNode["walkableHeight"].get_value<int>();
if (tileNode.contains("walkableClimb"))
tileOverride.walkableClimb = tileNode["walkableClimb"].get_value<int>();
if (tileNode.contains("maxSimplificationError"))
tileOverride.maxSimplificationError = tileNode["maxSimplificationError"].get_value<float>();
override.tileOverrides[{tileX, tileY}] = std::move(tileOverride);
}

View File

@@ -86,6 +86,7 @@ namespace MMAP
std::optional<int> walkableRadius;
std::optional<int> walkableHeight;
std::optional<int> walkableClimb;
std::optional<float> maxSimplificationError;
};
struct MapOverride {
@@ -95,6 +96,7 @@ namespace MMAP
std::optional<int> walkableClimb;
std::optional<int> vertexPerMapEdge;
std::optional<int> vertexPerTileEdge;
std::optional<float> maxSimplificationError;
// 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.

View File

@@ -30,6 +30,34 @@
namespace MMAP
{
static void modAlmostUnwalkableTriangles(float const playerSlopeAngle,
float const* verts, int /*nv*/,
int const* tris, int nt,
unsigned char* areas)
{
float const walkableThr = std::cos(playerSlopeAngle / 180.0f * static_cast<float>(M_PI));
float norm[3];
for (int i = 0; i < nt; ++i)
{
if (areas[i] == RC_NULL_AREA)
continue;
int const* tri = &tris[i * 3];
float e0[3], e1[3];
rcVsub(e0, &verts[tri[1] * 3], &verts[tri[0] * 3]);
rcVsub(e1, &verts[tri[2] * 3], &verts[tri[0] * 3]);
rcVcross(norm, e0, e1);
rcVnormalize(norm);
if (norm[1] <= walkableThr)
areas[i] = NAV_GROUND_STEEP;
}
}
TileBuilder::TileBuilder(MapBuilder* mapBuilder, bool skipLiquid, bool debugOutput) :
m_debugOutput(debugOutput),
m_mapBuilder(mapBuilder),
@@ -660,6 +688,8 @@ namespace MMAP
unsigned char* triFlags = new unsigned char[tTriCount];
memset(triFlags, NAV_GROUND, tTriCount * sizeof(unsigned char));
rcClearUnwalkableTriangles(m_rcContext, tileCfg.walkableSlopeAngle, tVerts, tVertCount, tTris, tTriCount, triFlags);
// mod_playerbots (bots should not attempt to use paths that includes slopes beyound 50 degrees)
modAlmostUnwalkableTriangles(50.0f, tVerts, tVertCount, tTris, tTriCount, triFlags);
rcRasterizeTriangles(m_rcContext, tVerts, tVertCount, tTris, triFlags, tTriCount, *tile.solid, config.walkableClimb);
delete[] triFlags;

View File

@@ -22,14 +22,14 @@ mmapsConfig:
# 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)
# cellSize = MMAP::GRID_SIZE / (vertexPerMapEdge - 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
# vertexPerMapEdge = number of vertices along one edge of the full map grid
#
# Example:
# If verticesPerMapEdge = 2000, then:
# If vertexPerMapEdge = 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),
@@ -47,7 +47,7 @@ mmapsConfig:
#
# Vanilla WotLK uses 6, which allows creatures to "jump" over fences.
# Classic WotLK uses 4, which forces creatures to walk around fences.
walkableClimb: 6
walkableClimb: 4
# Minimum distance (in cell units) around walkable surfaces.
# Helps prevent NPCs from clipping into walls and narrow gaps.
@@ -56,17 +56,17 @@ mmapsConfig:
# 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
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.
verticesPerTileEdge: 80
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.
maxSimplificationError: 1.8
maxSimplificationError: 0.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,