From f839009e9ad26651126f0c592d9f7f4cd914a28c Mon Sep 17 00:00:00 2001 From: Anton Popovichenko Date: Sun, 19 Jul 2026 13:34:04 +0200 Subject: [PATCH] fix(Core/MMaps): fix Blade's Edge Arena falling/edge pathing (bump mmap version to 20) (#25720) --- apps/installer/includes/functions.sh | 2 +- src/common/Collision/Maps/MapDefines.h | 2 +- src/server/game/Maps/AreaDefines.h | 1 + .../MovementGenerators/PathGenerator.cpp | 106 +++++++++++++++++- src/tools/mmaps_generator/Config.cpp | 19 +++- src/tools/mmaps_generator/Config.h | 4 + src/tools/mmaps_generator/Info/readme.txt | 5 - src/tools/mmaps_generator/MapBuilder.cpp | 8 +- src/tools/mmaps_generator/MapBuilder.h | 2 - src/tools/mmaps_generator/PathGenerator.cpp | 14 +-- src/tools/mmaps_generator/TerrainBuilder.cpp | 41 +++---- src/tools/mmaps_generator/TerrainBuilder.h | 2 +- src/tools/mmaps_generator/mmaps-config.yaml | 21 +++- 13 files changed, 168 insertions(+), 59 deletions(-) diff --git a/apps/installer/includes/functions.sh b/apps/installer/includes/functions.sh index 7f179e5c1..fce8cdb44 100644 --- a/apps/installer/includes/functions.sh +++ b/apps/installer/includes/functions.sh @@ -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" diff --git a/src/common/Collision/Maps/MapDefines.h b/src/common/Collision/Maps/MapDefines.h index a3527f16c..8f61275f2 100644 --- a/src/common/Collision/Maps/MapDefines.h +++ b/src/common/Collision/Maps/MapDefines.h @@ -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 { diff --git a/src/server/game/Maps/AreaDefines.h b/src/server/game/Maps/AreaDefines.h index be844f3c9..f722edd00 100644 --- a/src/server/game/Maps/AreaDefines.h +++ b/src/server/game/Maps/AreaDefines.h @@ -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, diff --git a/src/server/game/Movement/MovementGenerators/PathGenerator.cpp b/src/server/game/Movement/MovementGenerators/PathGenerator.cpp index 607428cb6..9f92d18f4 100644 --- a/src/server/game/Movement/MovementGenerators/PathGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/PathGenerator.cpp @@ -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 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); } } diff --git a/src/tools/mmaps_generator/Config.cpp b/src/tools/mmaps_generator/Config.cpp index dabdb0f0d..6dacd579f 100644 --- a/src/tools/mmaps_generator/Config.cpp +++ b/src/tools/mmaps_generator/Config.cpp @@ -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>(); + } + 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(); if (mapNode.contains("walkableClimb")) override.walkableClimb = mapNode["walkableClimb"].get_value(); - if (mapNode.contains("vertexPerMapEdge")) - override.vertexPerMapEdge = mapNode["vertexPerMapEdge"].get_value(); + if (mapNode.contains("verticesPerMapEdge")) + override.vertexPerMapEdge = mapNode["verticesPerMapEdge"].get_value(); + if (mapNode.contains("verticesPerTileEdge")) + override.vertexPerTileEdge = mapNode["verticesPerTileEdge"].get_value(); if (mapNode.contains("cellSizeHorizontal")) override.cellSizeHorizontal = mapNode["cellSizeHorizontal"].get_value(); if (mapNode.contains("cellSizeVertical")) diff --git a/src/tools/mmaps_generator/Config.h b/src/tools/mmaps_generator/Config.h index c97fa7999..d214450cc 100644 --- a/src/tools/mmaps_generator/Config.h +++ b/src/tools/mmaps_generator/Config.h @@ -76,6 +76,8 @@ namespace MMAP std::string MMapsPath() const { return (_dataDir / "mmaps").string(); } std::string DataDirPath() const { return _dataDir.string(); } + std::vector const& OffMeshConnections() const { return _offmeshConnections; } + private: explicit Config(); @@ -153,6 +155,8 @@ namespace MMAP bool _debugOutput; std::filesystem::path _dataDir; + + std::vector _offmeshConnections; }; } diff --git a/src/tools/mmaps_generator/Info/readme.txt b/src/tools/mmaps_generator/Info/readme.txt index c0702d50c..c6750aced 100644 --- a/src/tools/mmaps_generator/Info/readme.txt +++ b/src/tools/mmaps_generator/Info/readme.txt @@ -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. diff --git a/src/tools/mmaps_generator/MapBuilder.cpp b/src/tools/mmaps_generator/MapBuilder.cpp index 45c71367b..bd02d235c 100644 --- a/src/tools/mmaps_generator/MapBuilder.cpp +++ b/src/tools/mmaps_generator/MapBuilder.cpp @@ -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) diff --git a/src/tools/mmaps_generator/MapBuilder.h b/src/tools/mmaps_generator/MapBuilder.h index 313c88c52..12ffab06b 100644 --- a/src/tools/mmaps_generator/MapBuilder.h +++ b/src/tools/mmaps_generator/MapBuilder.h @@ -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; diff --git a/src/tools/mmaps_generator/PathGenerator.cpp b/src/tools/mmaps_generator/PathGenerator.cpp index b7fa572c1..581bb153b 100644 --- a/src/tools/mmaps_generator/PathGenerator.cpp +++ b/src/tools/mmaps_generator/PathGenerator.cpp @@ -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) diff --git a/src/tools/mmaps_generator/TerrainBuilder.cpp b/src/tools/mmaps_generator/TerrainBuilder.cpp index c85a7933e..5b27f40ce 100644 --- a/src/tools/mmaps_generator/TerrainBuilder.cpp +++ b/src/tools/mmaps_generator/TerrainBuilder.cpp @@ -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& 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); } } diff --git a/src/tools/mmaps_generator/TerrainBuilder.h b/src/tools/mmaps_generator/TerrainBuilder.h index 63c21fe08..04d0e78a5 100644 --- a/src/tools/mmaps_generator/TerrainBuilder.h +++ b/src/tools/mmaps_generator/TerrainBuilder.h @@ -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& offMeshLines); [[nodiscard]] bool usesLiquids() const { return !m_skipLiquid; } diff --git a/src/tools/mmaps_generator/mmaps-config.yaml b/src/tools/mmaps_generator/mmaps-config.yaml index 7adcee64c..49187c5b9 100644 --- a/src/tools/mmaps_generator/mmaps-config.yaml +++ b/src/tools/mmaps_generator/mmaps-config.yaml @@ -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.