fix(Core/MMaps): fix Blade's Edge Arena falling/edge pathing (bump mmap version to 20) (#25720)

This commit is contained in:
Anton Popovichenko
2026-07-19 13:34:04 +02:00
committed by GitHub
parent 98b06f6723
commit f839009e9a
13 changed files with 168 additions and 59 deletions

View File

@@ -155,7 +155,7 @@ function inst_simple_restarter {
function inst_download_client_data {
# change the following version when needed
local VERSION=v19
local VERSION=v20
echo "#######################"
echo "Client data downloader"

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
{

View File

@@ -236,6 +236,7 @@ enum MapIDs : uint32
MAP_AUCHINDOUN_MANA_TOMBS = 557,
MAP_AUCHINDOUN_AUCHENAI_CRYPTS = 558,
MAP_THE_ESCAPE_FROM_DURNHOLDE = 560,
MAP_BLADES_EDGE_ARENA = 562,
MAP_BLACK_TEMPLE = 564,
MAP_GRUULS_LAIR = 565,
MAP_EYE_OF_THE_STORM = 566,

View File

@@ -24,6 +24,104 @@
#include "Map.h"
#include "Metric.h"
// Blades Edge Arena Ropes normalization
namespace
{
constexpr float BLADE_EDGE_ROPE_SNAP_DIST = 1.5f;
constexpr float BLADE_EDGE_ROPE_SNAP_DIST2 = BLADE_EDGE_ROPE_SNAP_DIST * BLADE_EDGE_ROPE_SNAP_DIST;
struct BladeEdgeArenaRope
{
G3D::Vector3 Start;
G3D::Vector3 End;
float Sag;
};
static const std::array<BladeEdgeArenaRope, 2> BladeEdgeArenaRopes =
{{
{
{6243.1523f, 267.53094f, 10.929295f},
{6245.9717f, 271.29346f, 10.879172f},
0.43f
},
{
{6234.3213f, 256.29733f, 11.002348f},
{6231.3247f, 252.58781f, 10.976968f},
0.46f
}
}};
bool IsOutsideExpandedXYBounds(G3D::Vector3 const& point, BladeEdgeArenaRope const& rope)
{
float const minX = std::min(rope.Start.x, rope.End.x) - BLADE_EDGE_ROPE_SNAP_DIST;
float const maxX = std::max(rope.Start.x, rope.End.x) + BLADE_EDGE_ROPE_SNAP_DIST;
float const minY = std::min(rope.Start.y, rope.End.y) - BLADE_EDGE_ROPE_SNAP_DIST;
float const maxY = std::max(rope.Start.y, rope.End.y) + BLADE_EDGE_ROPE_SNAP_DIST;
return point.x < minX || point.x > maxX || point.y < minY || point.y > maxY;
}
bool GetClosestPointOnBladeEdgeArenaRope(G3D::Vector3 const& point, BladeEdgeArenaRope const& rope, G3D::Vector3& closestPoint)
{
G3D::Vector3 const ropeVector = rope.End - rope.Start;
float const ropeLength2XY = ropeVector.x * ropeVector.x + ropeVector.y * ropeVector.y;
if (ropeLength2XY < 0.00001f)
return false;
G3D::Vector3 const pointVector = point - rope.Start;
float t = (pointVector.x * ropeVector.x + pointVector.y * ropeVector.y) / ropeLength2XY;
t = std::clamp(t, 0.0f, 1.0f);
float const closestX = rope.Start.x + ropeVector.x * t;
float const closestY = rope.Start.y + ropeVector.y * t;
float const dx = point.x - closestX;
float const dy = point.y - closestY;
// If the point is already too far in XY, it cannot be within the 3D snap radius.
if (dx * dx + dy * dy >= BLADE_EDGE_ROPE_SNAP_DIST2)
return false;
float const linearZ = rope.Start.z + (rope.End.z - rope.Start.z) * t;
float const sagZ = rope.Sag * std::sin(M_PI * t);
closestPoint = { closestX, closestY, linearZ - sagZ };
return true;
}
bool TrySnapToBladeEdgeArenaRope(G3D::Vector3& point)
{
bool snapped = false;
float bestDist2 = BLADE_EDGE_ROPE_SNAP_DIST2;
G3D::Vector3 bestPoint;
for (BladeEdgeArenaRope const& rope : BladeEdgeArenaRopes)
{
if (IsOutsideExpandedXYBounds(point, rope))
continue;
G3D::Vector3 closestPoint;
if (!GetClosestPointOnBladeEdgeArenaRope(point, rope, closestPoint))
continue;
float const dist2 = (point - closestPoint).squaredLength();
if (dist2 < bestDist2)
{
bestDist2 = dist2;
bestPoint = closestPoint;
snapped = true;
}
}
if (snapped)
point = bestPoint;
return snapped;
}
}
////////////////// PathGenerator //////////////////
PathGenerator::PathGenerator(WorldObject const* owner) :
_polyLength(0), _type(PATHFIND_BLANK), _useStraightPath(false), _forceDestination(false),
@@ -622,9 +720,13 @@ void PathGenerator::BuildPointPath(const float* startPoint, const float* endPoin
void PathGenerator::NormalizePath()
{
for (uint32 i = 0; i < _pathPoints.size(); ++i)
bool const snapBladeEdgeArenaRopes = _source->GetMapId() == MAP_BLADES_EDGE_ARENA;
for (G3D::Vector3& point : _pathPoints)
{
_source->UpdateAllowedPositionZ(_pathPoints[i].x, _pathPoints[i].y, _pathPoints[i].z);
if (snapBladeEdgeArenaRopes && TrySnapToBladeEdgeArenaRope(point))
continue;
_source->UpdateAllowedPositionZ(point.x, point.y, point.z);
}
}

View File

@@ -191,6 +191,15 @@ namespace MMAP
tryBoolean(mmapsNode, "skipBattlegrounds", _skipBattlegrounds);
tryBoolean(mmapsNode, "debugOutput", _debugOutput);
if (mmapsNode.contains("offmeshConnections") && mmapsNode["offmeshConnections"].is_sequence())
{
_offmeshConnections = mmapsNode["offmeshConnections"].get_value<std::vector<std::string>>();
}
else
{
_offmeshConnections.clear();
}
std::string dataDirPath;
tryString(mmapsNode, "dataDir", dataDirPath);
_dataDir = dataDirPath;
@@ -202,8 +211,8 @@ namespace MMAP
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);
tryInt(mmapsNode, "verticesPerMapEdge", _global.vertexPerMapEdge);
tryInt(mmapsNode, "verticesPerTileEdge", _global.vertexPerTileEdge);
tryFloat(mmapsNode, "maxSimplificationError", _global.maxSimplificationError);
// Map overrides
@@ -225,8 +234,10 @@ namespace MMAP
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("verticesPerMapEdge"))
override.vertexPerMapEdge = mapNode["verticesPerMapEdge"].get_value<int>();
if (mapNode.contains("verticesPerTileEdge"))
override.vertexPerTileEdge = mapNode["verticesPerTileEdge"].get_value<int>();
if (mapNode.contains("cellSizeHorizontal"))
override.cellSizeHorizontal = mapNode["cellSizeHorizontal"].get_value<float>();
if (mapNode.contains("cellSizeVertical"))

View File

@@ -76,6 +76,8 @@ namespace MMAP
std::string MMapsPath() const { return (_dataDir / "mmaps").string(); }
std::string DataDirPath() const { return _dataDir.string(); }
std::vector<std::string> const& OffMeshConnections() const { return _offmeshConnections; }
private:
explicit Config();
@@ -153,6 +155,8 @@ namespace MMAP
bool _debugOutput;
std::filesystem::path _dataDir;
std::vector<std::string> _offmeshConnections;
};
}

View File

@@ -6,11 +6,6 @@ Generator command line args
--threads [#] Max number of threads used by the generator
Default: 3
--offMeshInput [file.*] Path to file containing off mesh connections data.
Format must be: (see offmesh_example.txt)
"map_id tile_x,tile_y (start_x start_y start_z) (end_x end_y end_z) size //optional comments"
Single mesh connection per line.
--silent [] Make us script friendly. Do not wait for user input
on error or completion.

View File

@@ -55,10 +55,9 @@ namespace MMAP
m_workerThread.join();
}
MapBuilder::MapBuilder(Config* config, int mapid, const char* offMeshFilePath, unsigned int threads) :
MapBuilder::MapBuilder(Config* config, int mapid, unsigned int threads) :
m_config (config),
m_debugOutput (config->IsDebugOutputEnabled()),
m_offMeshFilePath (offMeshFilePath),
m_threads (threads),
m_skipContinents (config->ShouldSkipContinents()),
m_skipJunkMaps (config->ShouldSkipJunkMaps()),
@@ -497,8 +496,7 @@ namespace MMAP
// 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_offMeshFilePath);
m_terrainBuilder->loadOffMeshConnections(mapID, tileX, tileY, meshData, m_mapBuilder->getConfig().OffMeshConnections());
// build navmesh tile
buildMoveMapTile(mapID, tileX, tileY, meshData, bmin, bmax, navMesh);
@@ -818,7 +816,7 @@ namespace MMAP
}
if (params.vertCount >= 0xffff)
{
printf("%s Too many vertices! \n", tileString);
printf("%s Too many vertices! %d out of %d! \n", tileString, params.vertCount, 0xffff);
break;
}
if (!params.vertCount || !params.verts)

View File

@@ -125,7 +125,6 @@ namespace MMAP
public:
MapBuilder(Config* config,
int mapid,
char const* offMeshFilePath,
unsigned int threads);
~MapBuilder();
@@ -167,7 +166,6 @@ namespace MMAP
bool m_debugOutput;
const char* m_offMeshFilePath;
unsigned int m_threads;
bool m_skipContinents;
bool m_skipJunkMaps;

View File

@@ -63,7 +63,6 @@ bool handleArgs(int argc, char** argv,
int& tileY,
std::string& configFilePath,
bool& silent,
char*& offMeshInputPath,
char*& file,
unsigned int& threads)
{
@@ -120,14 +119,6 @@ bool handleArgs(int argc, char** argv,
{
silent = true;
}
else if (strcmp(argv[i], "--offMeshInput") == 0)
{
param = argv[++i];
if (!param)
return false;
offMeshInputPath = param;
}
else
{
int map = atoi(argv[i]);
@@ -174,11 +165,10 @@ int main(int argc, char** argv)
int mapnum = -1;
int tileX = -1, tileY = -1;
bool silent = false;
char* offMeshInputPath = nullptr;
char* file = nullptr;
std::string configFilePath = "mmaps-config.yaml";
bool validParam = handleArgs(argc, argv, mapnum,
tileX, tileY, configFilePath, silent, offMeshInputPath, file, threads);
tileX, tileY, configFilePath, silent, file, threads);
if (!validParam)
return silent ? -1 : finish("You have specified invalid parameters", -1);
@@ -202,7 +192,7 @@ int main(int argc, char** argv)
if (!checkDirectories(config->DataDirPath(), config->IsDebugOutputEnabled()))
return silent ? -3 : finish("Press ENTER to close...", -3);
MapBuilder builder(&config.value(), mapnum, offMeshInputPath, threads);
MapBuilder builder(&config.value(), mapnum, threads);
uint32 start = getMSTime();
if (file)

View File

@@ -927,30 +927,29 @@ namespace MMAP
}
/**************************************************************************/
void TerrainBuilder::loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, const char* offMeshFilePath)
void TerrainBuilder::loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY,
MeshData& meshData,
const std::vector<std::string>& offMeshLines)
{
// no meshfile input given?
if (!offMeshFilePath)
if (offMeshLines.empty())
return;
FILE* fp = fopen(offMeshFilePath, "rb");
if (!fp)
{
printf(" loadOffMeshConnections:: input file %s not found!\n", offMeshFilePath);
return;
}
// pretty silly thing, as we parse entire file and load only the tile we need
// but we don't expect this file to be too large
char* buf = new char[512];
while (fgets(buf, 512, fp))
for (const std::string& line : offMeshLines)
{
float p0[3], p1[3];
uint32 mid, tx, ty;
float size;
if (sscanf(buf, "%u %u,%u (%f %f %f) (%f %f %f) %f", &mid, &tx, &ty,
&p0[0], &p0[1], &p0[2], &p1[0], &p1[1], &p1[2], &size) != 10)
if (sscanf(line.c_str(),
"%u %u,%u (%f %f %f) (%f %f %f) %f",
&mid, &tx, &ty,
&p0[0], &p0[1], &p0[2],
&p1[0], &p1[1], &p1[2],
&size) != 10)
{
printf("Skipped off-mesh connection '%s': invalid format\n", line.c_str());
continue;
}
if (mapID == mid && tileX == tx && tileY == ty)
{
@@ -962,15 +961,11 @@ namespace MMAP
meshData.offMeshConnections.append(p1[2]);
meshData.offMeshConnections.append(p1[0]);
meshData.offMeshConnectionDirs.append(1); // 1 - both direction, 0 - one sided
meshData.offMeshConnectionRads.append(size); // agent size equivalent
// can be used same way as polygon flags
meshData.offMeshConnectionDirs.append(1); // 1 - both direction, 0 - one sided
meshData.offMeshConnectionRads.append(size); // agent radius equivalent
meshData.offMeshConnectionsAreas.append((unsigned char)0xFF);
meshData.offMeshConnectionsFlags.append((unsigned short)0xFF); // all movement masks can make this path
meshData.offMeshConnectionsFlags.append((unsigned short)0xFF);
}
}
delete [] buf;
fclose(fp);
}
}

View File

@@ -83,7 +83,7 @@ namespace MMAP
void loadMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData);
bool loadVMap(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData);
void loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, const char* offMeshFilePath);
void loadOffMeshConnections(uint32 mapID, uint32 tileX, uint32 tileY, MeshData& meshData, const std::vector<std::string>& offMeshLines);
[[nodiscard]] bool usesLiquids() const { return !m_skipLiquid; }

View File

@@ -9,6 +9,24 @@ mmapsConfig:
# and is also where the "mmaps" folder will be created or located.
dataDir: "./"
# Off-mesh connections define manual navigation links that are not part of the generated navmesh.
# They are used to connect two arbitrary points in the world where normal pathfinding cannot reach,
# such as jumps, ropes, ladders, teleports, elevators, or special scripted movement paths.
#
# Format:
# mapID tileX,tileY (start_x start_y start_z) (end_x end_y end_z) size
#
# Fields:
# mapID - Map identifier where this connection exists.
# tileX,tileY- Navmesh tile coordinates the connection belongs to.
# start - World position where the connection begins.
# end - World position where the connection ends.
# size - Effective radius of the connection (agent clearance / usability width).
offmeshConnections:
# Make Blades Edge Arena Ropes wider
- "562 31,20 (6234.474121 256.563721 11.063726) (6230.162598 251.681976 11.199670) 2.1"
- "562 31,20 (6242.273926 266.697540 11.090456) (6246.688965 272.064819 11.235604) 2.1"
meshSettings:
# Here we have global config for recast navigation.
# It's possible to override these data on map or tile level (see mapsOverrides).
@@ -106,9 +124,6 @@ mmapsConfig:
# 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.