From 063b53dbe5adee8f0fc76a5b341d4c30f808e677 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sun, 11 Jun 2023 13:30:59 +0300 Subject: [PATCH 01/17] Refactored AStar implementation Pt.1 Current A* implementation is tailored for single unit/single path requests. But can provide a wider functionality, like calculation of shared flow field, or fast distance transform or group path finding. Current changes are trying to make this component more generic: - A* wave propagation now uses separate predicate function, which provides checks for goal and heuristic costs - Path tracing moved to a standalone function --- src/astar.cpp | 261 +++++++++++++++++++++++++++++++------------------- 1 file changed, 163 insertions(+), 98 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index 5c30dd78813..88d6aade550 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -193,7 +193,10 @@ struct PathfindContext map.resize(static_cast(mapWidth) * static_cast(mapHeight)); // Allocate space for map, if needed. } - PathCoord tileS; // Start tile for pathfinding. (May be either source or target tile.) + /// Start tile for pathfinding. + /// It is used for context caching and reusing existing contexts for other units. + /// (May be either source or target tile.) + PathCoord tileS; uint32_t myGameTime; PathCoord nearestCoord; // Nearest reachable tile to destination. @@ -270,7 +273,7 @@ static inline unsigned WZ_DECL_PURE fpathGoodEstimate(PathCoord s, PathCoord f) /** Generate a new node */ -static inline void fpathNewNode(PathfindContext &context, PathCoord dest, PathCoord pos, unsigned prevDist, PathCoord prevPos) +static inline void fpathNewNode(PathfindContext &context, unsigned estimateCost, PathCoord pos, unsigned prevDist, PathCoord prevPos) { ASSERT_OR_RETURN(, (unsigned)pos.x < (unsigned)mapWidth && (unsigned)pos.y < (unsigned)mapHeight, "X (%d) or Y (%d) coordinate for path finding node is out of range!", pos.x, pos.y); @@ -279,7 +282,7 @@ static inline void fpathNewNode(PathfindContext &context, PathCoord dest, PathCo unsigned costFactor = context.isDangerous(pos.x, pos.y) ? 5 : 1; node.p = pos; node.dist = prevDist + fpathEstimate(prevPos, pos) * costFactor; - node.est = node.dist + fpathGoodEstimate(pos, dest); + node.est = node.dist + estimateCost; Vector2i delta = Vector2i(pos.x - prevPos.x, pos.y - prevPos.y) * 64; bool isDiagonal = delta.x && delta.y; @@ -352,15 +355,47 @@ static void fpathAStarReestimate(PathfindContext &context, PathCoord tileF) std::make_heap(context.nodes.begin(), context.nodes.end()); } + /// Returns nearest explored tile to tileF. -static PathCoord fpathAStarExplore(PathfindContext &context, PathCoord tileF) -{ - PathCoord nearestCoord(0, 0); - unsigned nearestDist = 0xFFFFFFFF; +struct NearestSearchPredicate { + /// Target tile. + PathCoord goal; + /// Nearest coordinates of the wave to the target tile. + PathCoord nearestCoord {0, 0}; + /// Nearest distance to the target. + unsigned nearestDist = 0xFFFFFFFF; + + NearestSearchPredicate(const PathCoord& goal) : goal(goal) { } + + bool isGoal(const PathNode& node) { + if (node.p == goal) + { + // reached the target + nearestCoord = node.p; + nearestDist = 0; + return true; + } else if (node.est - node.dist < nearestDist) + { + nearestCoord = node.p; + nearestDist = node.est - node.dist; + } + return false; + } + + unsigned estimateCost(const PathCoord& pos) const { + return fpathGoodEstimate(pos, goal); + } +}; - // search for a route - bool foundIt = false; - while (!context.nodes.empty() && !foundIt) +/// Runs A* wave propagation for 8 neighbors pattern. +/// Target is checked using predicate object. +/// @returns true if search wave has reached the goal or false if +/// the wave has collapsed before reaching the goal. +template +static bool fpathAStarExplore(PathfindContext &context, Predicate& predicate) +{ + constexpr int adjacency = 8; + while (!context.nodes.empty()) { PathNode node = fpathTakeNode(context.nodes); if (context.map[node.p.x + node.p.y * mapWidth].visited) @@ -369,67 +404,58 @@ static PathCoord fpathAStarExplore(PathfindContext &context, PathCoord tileF) } context.map[node.p.x + node.p.y * mapWidth].visited = true; - // note the nearest node to the target so far - if (node.est - node.dist < nearestDist) - { - nearestCoord = node.p; - nearestDist = node.est - node.dist; + if (predicate.isGoal(node)) + return true; + + /* + 5 6 7 + \|/ + 4 -I- 0 + /|\ + 3 2 1 + odd:orthogonal-adjacent tiles even:non-orthogonal-adjacent tiles + */ + + // Cached state from blocking map. + bool blocking[adjacency]; + bool ignoreBlocking[adjacency]; + for (unsigned dir = 0; dir < adjacency; ++dir) { + int x = node.p.x + aDirOffset[dir].x; + int y = node.p.y + aDirOffset[dir].y; + blocking[dir] = context.isBlocked(x, y); + ignoreBlocking[dir] = context.dstIgnore.isNonblocking(x, y); } - if (node.p == tileF) - { - // reached the target - nearestCoord = node.p; - foundIt = true; // Break out of loop, but not before inserting neighbour nodes, since the neighbours may be important if the context gets reused. - } + bool ignoreCenter = context.dstIgnore.isNonblocking(node.p.x, node.p.y); // loop through possible moves in 8 directions to find a valid move - for (unsigned dir = 0; dir < ARRAY_SIZE(aDirOffset); ++dir) + for (unsigned dir = 0; dir < adjacency; ++dir) { - // Try a new location - int x = node.p.x + aDirOffset[dir].x; - int y = node.p.y + aDirOffset[dir].y; - - /* - 5 6 7 - \|/ - 4 -I- 0 - /|\ - 3 2 1 - odd:orthogonal-adjacent tiles even:non-orthogonal-adjacent tiles - */ - if (dir % 2 != 0 && !context.dstIgnore.isNonblocking(node.p.x, node.p.y) && !context.dstIgnore.isNonblocking(x, y)) + // See if the node is a blocking tile + if (blocking[dir]) + continue; + if (dir % 2 != 0 && !ignoreCenter && !ignoreBlocking[dir]) { - int x2, y2; - - // We cannot cut corners - x2 = node.p.x + aDirOffset[(dir + 1) % 8].x; - y2 = node.p.y + aDirOffset[(dir + 1) % 8].y; - if (context.isBlocked(x2, y2)) - { + // Turn CCW. + if (blocking[(dir + 1) % 8]) continue; - } - x2 = node.p.x + aDirOffset[(dir + 7) % 8].x; - y2 = node.p.y + aDirOffset[(dir + 7) % 8].y; - if (context.isBlocked(x2, y2)) - { + // Turn CW. + if (blocking[(dir + 7) % 8]) continue; - } } - // See if the node is a blocking tile - if (context.isBlocked(x, y)) - { - // tile is blocked, skip it - continue; - } + // Try a new location + int x = node.p.x + aDirOffset[dir].x; + int y = node.p.y + aDirOffset[dir].y; + PathCoord newPos(x, y); + unsigned estimateCost = predicate.estimateCost(newPos); // Now insert the point into the appropriate list, if not already visited. - fpathNewNode(context, tileF, PathCoord(x, y), node.dist, node.p); + fpathNewNode(context, estimateCost, newPos, node.dist, node.p); } } - return nearestCoord; + return false; } static void fpathInitContext(PathfindContext &context, std::shared_ptr &blockingMap, PathCoord tileS, PathCoord tileRealS, PathCoord tileF, PathNonblockingArea dstIgnore) @@ -437,10 +463,50 @@ static void fpathInitContext(PathfindContext &context, std::shared_ptr& path) { + ASR_RETVAL retval = ASR_OK; + path.clear(); + Vector2i newP(0, 0); + for (Vector2i p(world_coord(src.x) + TILE_UNITS / 2, world_coord(src.y) + TILE_UNITS / 2); true; p = newP) + { + ASSERT_OR_RETURN(ASR_FAILED, worldOnMap(p.x, p.y), "Assigned XY coordinates (%d, %d) not on map!", (int)p.x, (int)p.y); + ASSERT_OR_RETURN(ASR_FAILED, path.size() < (static_cast(mapWidth) * static_cast(mapHeight)), "Pathfinding got in a loop."); + + path.push_back(p); + + const PathExploredTile &tile = context.map[map_coord(p.x) + map_coord(p.y) * mapWidth]; + newP = p - Vector2i(tile.dx, tile.dy) * (TILE_UNITS / 64); + Vector2i mapP = map_coord(newP); + int xSide = newP.x - world_coord(mapP.x) > TILE_UNITS / 2 ? 1 : -1; // 1 if newP is on right-hand side of the tile, or -1 if newP is on the left-hand side of the tile. + int ySide = newP.y - world_coord(mapP.y) > TILE_UNITS / 2 ? 1 : -1; // 1 if newP is on bottom side of the tile, or -1 if newP is on the top side of the tile. + if (context.isBlocked(mapP.x + xSide, mapP.y)) + { + newP.x = world_coord(mapP.x) + TILE_UNITS / 2; // Point too close to a blocking tile on left or right side, so move the point to the middle. + } + if (context.isBlocked(mapP.x, mapP.y + ySide)) + { + newP.y = world_coord(mapP.y) + TILE_UNITS / 2; // Point too close to a blocking tile on rop or bottom side, so move the point to the middle. + } + if (map_coord(p) == Vector2i(dst.x, dst.y) || p == newP) + { + break; // We stopped moving, because we reached the destination or the closest reachable tile to dst. Give up now. + } + } + return retval; +} + +#if DEBUG +extern uint32_t selectedPlayer; +#endif + ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) { ASR_RETVAL retval = ASR_OK; @@ -451,30 +517,43 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) const PathCoord tileDest(map_coord(psJob->destX), map_coord(psJob->destY)); const PathNonblockingArea dstIgnore(psJob->dstStructure); +#if DEBUG + const DROID *psDroid = IdToDroid(psJob->droidID, selectedPlayer); + const bool isDroidSelected = (psDroid && psDroid->selected); +#endif + PathCoord endCoord; // Either nearest coord (mustReverse = true) or orig (mustReverse = false). std::list::iterator contextIterator = fpathContexts.begin(); - for (contextIterator = fpathContexts.begin(); contextIterator != fpathContexts.end(); ++contextIterator) + for (; contextIterator != fpathContexts.end(); ++contextIterator) { - if (!contextIterator->matches(psJob->blockingMap, tileDest, dstIgnore)) + PathfindContext& pfContext = *contextIterator; + if (!pfContext.matches(psJob->blockingMap, tileDest, dstIgnore)) { // This context is not for the same droid type and same destination. continue; } // We have tried going to tileDest before. - - if (contextIterator->map[tileOrig.x + tileOrig.y * mapWidth].iteration == contextIterator->iteration - && contextIterator->map[tileOrig.x + tileOrig.y * mapWidth].visited) + if (pfContext.map[tileOrig.x + tileOrig.y * mapWidth].iteration == pfContext.iteration + && pfContext.map[tileOrig.x + tileOrig.y * mapWidth].visited) { // Already know the path from orig to dest. endCoord = tileOrig; } + else if (pfContext.nodes.empty()) { + // Wave has already collapsed. Consequent attempt to search will exit immediately. + continue; + } else { // Need to find the path from orig to dest, continue previous exploration. - fpathAStarReestimate(*contextIterator, tileOrig); - endCoord = fpathAStarExplore(*contextIterator, tileOrig); + fpathAStarReestimate(pfContext, tileOrig); + NearestSearchPredicate pred(tileDest); + if (!fpathAStarExplore(pfContext, pred)) { + syncDebug("fpathAStarRoute (%d,%d) to (%d,%d) - wave collapsed. Nearest=%d", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y, pred.nearestDist); + } + endCoord = pred.nearestCoord; } if (endCoord != tileOrig) @@ -490,18 +569,28 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) if (contextIterator == fpathContexts.end()) { // We did not find an appropriate context. Make one. - if (fpathContexts.size() < 30) { - fpathContexts.push_back(PathfindContext()); + fpathContexts.emplace_back(); } - --contextIterator; + contextIterator--; + PathfindContext& pfContext = fpathContexts.back(); // Init a new context, overwriting the oldest one if we are caching too many. // We will be searching from orig to dest, since we don't know where the nearest reachable tile to dest is. - fpathInitContext(*contextIterator, psJob->blockingMap, tileOrig, tileOrig, tileDest, dstIgnore); - endCoord = fpathAStarExplore(*contextIterator, tileDest); - contextIterator->nearestCoord = endCoord; + fpathInitContext(pfContext, psJob->blockingMap, tileOrig, tileOrig, tileDest, dstIgnore); + + NearestSearchPredicate pred(tileDest); + if (!fpathAStarExplore(pfContext, pred)) { +#if DEBUG + if (isDroidSelected) { + pred = NearestSearchPredicate(tileDest); + fpathAStarExplore(pfContext, pred); + } +#endif + } + endCoord = pred.nearestCoord; + pfContext.nearestCoord = endCoord; } PathfindContext &context = *contextIterator; @@ -512,36 +601,12 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) retval = ASR_NEAREST; } - // Get route, in reverse order. - static std::vector path; // Declared static to save allocations. - path.clear(); - Vector2i newP(0, 0); - for (Vector2i p(world_coord(endCoord.x) + TILE_UNITS / 2, world_coord(endCoord.y) + TILE_UNITS / 2); true; p = newP) - { - ASSERT_OR_RETURN(ASR_FAILED, worldOnMap(p.x, p.y), "Assigned XY coordinates (%d, %d) not on map!", (int)p.x, (int)p.y); - ASSERT_OR_RETURN(ASR_FAILED, path.size() < (static_cast(mapWidth) * static_cast(mapHeight)), "Pathfinding got in a loop."); - - path.push_back(p); + static std::vector path; // Declared static to save allocations. + ASR_RETVAL traceRet = fpathTracePath(context, endCoord, tileOrig, path); + if (traceRet != ASR_OK) + return traceRet; - PathExploredTile &tile = context.map[map_coord(p.x) + map_coord(p.y) * mapWidth]; - newP = p - Vector2i(tile.dx, tile.dy) * (TILE_UNITS / 64); - Vector2i mapP = map_coord(newP); - int xSide = newP.x - world_coord(mapP.x) > TILE_UNITS / 2 ? 1 : -1; // 1 if newP is on right-hand side of the tile, or -1 if newP is on the left-hand side of the tile. - int ySide = newP.y - world_coord(mapP.y) > TILE_UNITS / 2 ? 1 : -1; // 1 if newP is on bottom side of the tile, or -1 if newP is on the top side of the tile. - if (context.isBlocked(mapP.x + xSide, mapP.y)) - { - newP.x = world_coord(mapP.x) + TILE_UNITS / 2; // Point too close to a blocking tile on left or right side, so move the point to the middle. - } - if (context.isBlocked(mapP.x, mapP.y + ySide)) - { - newP.y = world_coord(mapP.y) + TILE_UNITS / 2; // Point too close to a blocking tile on rop or bottom side, so move the point to the middle. - } - if (map_coord(p) == Vector2i(context.tileS.x, context.tileS.y) || p == newP) - { - break; // We stopped moving, because we reached the destination or the closest reachable tile to context.tileS. Give up now. - } - } if (retval == ASR_OK) { // Found exact path, so use exact coordinates for last point, no reason to lose precision From 050a52ab237e292be02c0b4427d4d52c4a712794 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Mon, 12 Jun 2023 20:39:40 +0300 Subject: [PATCH 02/17] Reusing same predicate for path query --- src/astar.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index 88d6aade550..360545f03c5 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -385,6 +385,11 @@ struct NearestSearchPredicate { unsigned estimateCost(const PathCoord& pos) const { return fpathGoodEstimate(pos, goal); } + + void clear() { + nearestCoord = {0, 0}; + nearestDist = 0xFFFFFFFF; + } }; /// Runs A* wave propagation for 8 neighbors pattern. @@ -522,6 +527,8 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) const bool isDroidSelected = (psDroid && psDroid->selected); #endif + NearestSearchPredicate pred(tileDest); + PathCoord endCoord; // Either nearest coord (mustReverse = true) or orig (mustReverse = false). std::list::iterator contextIterator = fpathContexts.begin(); @@ -549,7 +556,7 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) { // Need to find the path from orig to dest, continue previous exploration. fpathAStarReestimate(pfContext, tileOrig); - NearestSearchPredicate pred(tileDest); + pred.clear(); if (!fpathAStarExplore(pfContext, pred)) { syncDebug("fpathAStarRoute (%d,%d) to (%d,%d) - wave collapsed. Nearest=%d", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y, pred.nearestDist); } @@ -580,7 +587,7 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) // We will be searching from orig to dest, since we don't know where the nearest reachable tile to dest is. fpathInitContext(pfContext, psJob->blockingMap, tileOrig, tileOrig, tileDest, dstIgnore); - NearestSearchPredicate pred(tileDest); + pred.clear(); if (!fpathAStarExplore(pfContext, pred)) { #if DEBUG if (isDroidSelected) { From 83a046e34840aec869869e3c6e2052a6b3832de8 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Mon, 12 Jun 2023 21:13:48 +0300 Subject: [PATCH 03/17] Removed nearestCoord field from PathfindContext --- src/astar.cpp | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index 360545f03c5..0bfc1c835c1 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -199,8 +199,6 @@ struct PathfindContext PathCoord tileS; uint32_t myGameTime; - PathCoord nearestCoord; // Nearest reachable tile to destination. - /** Counter to implement lazy deletion from map. * * @see fpathTableReset @@ -557,20 +555,14 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) // Need to find the path from orig to dest, continue previous exploration. fpathAStarReestimate(pfContext, tileOrig); pred.clear(); - if (!fpathAStarExplore(pfContext, pred)) { + if (fpathAStarExplore(pfContext, pred)) { + endCoord = pred.nearestCoord; + mustReverse = false; // We have the path from the nearest reachable tile to dest, to orig. + break; // Found the path! Don't search more contexts. + } else { syncDebug("fpathAStarRoute (%d,%d) to (%d,%d) - wave collapsed. Nearest=%d", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y, pred.nearestDist); } - endCoord = pred.nearestCoord; - } - - if (endCoord != tileOrig) - { - // orig turned out to be on a different island than what this context was used for, so can't use this context data after all. - continue; } - - mustReverse = false; // We have the path from the nearest reachable tile to dest, to orig. - break; // Found the path! Don't search more contexts. } if (contextIterator == fpathContexts.end()) @@ -597,13 +589,12 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) #endif } endCoord = pred.nearestCoord; - pfContext.nearestCoord = endCoord; } PathfindContext &context = *contextIterator; // return the nearest route if no actual route was found - if (context.nearestCoord != tileDest) + if (endCoord != tileDest) { retval = ASR_NEAREST; } @@ -650,7 +641,7 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) if (!context.isBlocked(tileOrig.x, tileOrig.y)) // If blocked, searching from tileDest to tileOrig wouldn't find the tileOrig tile. { // Next time, search starting from nearest reachable tile to the destination. - fpathInitContext(context, psJob->blockingMap, tileDest, context.nearestCoord, tileOrig, dstIgnore); + fpathInitContext(context, psJob->blockingMap, tileDest, pred.nearestCoord, tileOrig, dstIgnore); } } else From 9c3091d6999ddb9de552955860e008cc37dd2526 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Mon, 12 Jun 2023 21:35:53 +0300 Subject: [PATCH 04/17] AStar uses direct poitner to blocking map during search --- src/astar.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index 0bfc1c835c1..e8824dd4736 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -157,7 +157,7 @@ struct PathNonblockingArea // Data structures used for pathfinding, can contain cached results. struct PathfindContext { - PathfindContext() : myGameTime(0), iteration(0), blockingMap(nullptr) {} + PathfindContext() : myGameTime(0), iteration(0), blockingMap(nullptr), pBlockingMap(nullptr) {} bool isBlocked(int x, int y) const { if (dstIgnore.isNonblocking(x, y)) @@ -165,11 +165,11 @@ struct PathfindContext return false; // The path is actually blocked here by a structure, but ignore it since it's where we want to go (or where we came from). } // Not sure whether the out-of-bounds check is needed, can only happen if pathfinding is started on a blocking tile (or off the map). - return x < 0 || y < 0 || x >= mapWidth || y >= mapHeight || blockingMap->map[x + y * mapWidth]; + return x < 0 || y < 0 || x >= mapWidth || y >= mapHeight || pBlockingMap->map[x + y * mapWidth]; } bool isDangerous(int x, int y) const { - return !blockingMap->dangerMap.empty() && blockingMap->dangerMap[x + y * mapWidth]; + return !pBlockingMap->dangerMap.empty() && pBlockingMap->dangerMap[x + y * mapWidth]; } bool matches(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_) const { @@ -179,6 +179,7 @@ struct PathfindContext void assign(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_) { blockingMap = blockingMap_; + pBlockingMap = blockingMap.get(); tileS = tileS_; dstIgnore = dstIgnore_; myGameTime = blockingMap->type.gameTime; @@ -208,6 +209,7 @@ struct PathfindContext std::vector nodes; ///< Edge of explored region of the map. std::vector map; ///< Map, with paths leading back to tileS. std::shared_ptr blockingMap; ///< Map of blocking tiles for the type of object which needs a path. + PathBlockingMap* pBlockingMap; ///< Direct pointer to blocking map. Using shared pointer in searching loop can hurt performance. PathNonblockingArea dstIgnore; ///< Area of structure at destination which should be considered nonblocking. }; @@ -354,7 +356,7 @@ static void fpathAStarReestimate(PathfindContext &context, PathCoord tileF) } -/// Returns nearest explored tile to tileF. +/// A predicate for searching path to a single point. struct NearestSearchPredicate { /// Target tile. PathCoord goal; @@ -558,7 +560,7 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) if (fpathAStarExplore(pfContext, pred)) { endCoord = pred.nearestCoord; mustReverse = false; // We have the path from the nearest reachable tile to dest, to orig. - break; // Found the path! Don't search more contexts. + break; // Found the path! Don't search more contexts. } else { syncDebug("fpathAStarRoute (%d,%d) to (%d,%d) - wave collapsed. Nearest=%d", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y, pred.nearestDist); } @@ -580,14 +582,7 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) fpathInitContext(pfContext, psJob->blockingMap, tileOrig, tileOrig, tileDest, dstIgnore); pred.clear(); - if (!fpathAStarExplore(pfContext, pred)) { -#if DEBUG - if (isDroidSelected) { - pred = NearestSearchPredicate(tileDest); - fpathAStarExplore(pfContext, pred); - } -#endif - } + fpathAStarExplore(pfContext, pred); endCoord = pred.nearestCoord; } From 32d6c5ab9a6c9b3b285d9a46afbb14df2c1df85a Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Thu, 20 Jul 2023 20:01:01 +0300 Subject: [PATCH 05/17] Batch job processing in PF thread --- src/fpath.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/fpath.cpp b/src/fpath.cpp index e3b7d3d9736..0c6aa7d32bc 100644 --- a/src/fpath.cpp +++ b/src/fpath.cpp @@ -75,6 +75,8 @@ static int fpathThreadFunc(void *) { wzMutexLock(fpathMutex); + std::list localPathJobs; + while (!fpathQuit) { if (pathJobs.empty()) @@ -85,15 +87,19 @@ static int fpathThreadFunc(void *) wzMutexLock(fpathMutex); continue; } - - // Copy the first job from the queue. - packagedPathJob job = std::move(pathJobs.front()); - pathJobs.pop_front(); - + // Take batch of jobs to process them without additional synchronizations. + localPathJobs = std::move(pathJobs); wzMutexUnlock(fpathMutex); - job(); + { + WZ_PROFILE_SCOPE(fpathJob); + while(!localPathJobs.empty()) { + // Process jobs one by one + packagedPathJob job = std::move(localPathJobs.front()); + localPathJobs.pop_front(); + job(); + } + } wzMutexLock(fpathMutex); - waitingForResult = false; objTrace(waitingForResultId, "These are the droids you are looking for."); wzSemaphorePost(waitingForResultSemaphore); @@ -121,7 +127,6 @@ bool fpathInitialise() return true; } - void fpathShutdown() { if (fpathThread) From 27c27e6647a5343e2b3a24f48da88f11f1ae91c0 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Thu, 20 Jul 2023 22:12:28 +0300 Subject: [PATCH 06/17] Refactor A* core 1. Wrapped PF map cache into a standalone structure/instance 2. Moved all global state out of astar.cpp. 3. Using backward path search. It provides more opportunities for reusing previous contexts. 4. Introduced cost accessor to A* wave exploration --- src/astar.cpp | 524 +++++++++++++++++++++++--------------------------- src/astar.h | 194 ++++++++++++++++++- src/fpath.cpp | 60 +++--- 3 files changed, 460 insertions(+), 318 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index e8824dd4736..96ae4ab5547 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -40,190 +40,20 @@ * stored in the PathExploredTile 2D array of tiles. */ +#include + #ifndef WZ_TESTING #include "lib/framework/frame.h" +#include "fpath.h" #include "astar.h" #include "map.h" #endif -#include -#include -#include -#include - -#include "lib/netplay/netplay.h" - -/// A coordinate. -struct PathCoord -{ - PathCoord(): x(0), y(0) {} - PathCoord(int16_t x_, int16_t y_) : x(x_), y(y_) {} - bool operator ==(PathCoord const &z) const - { - return x == z.x && y == z.y; - } - bool operator !=(PathCoord const &z) const - { - return !(*this == z); - } - - int16_t x, y; -}; - -/** The structure to store a node of the route in node table - * - * @ingroup pathfinding - */ -struct PathNode -{ - bool operator <(PathNode const &z) const - { - // Sort descending est, fallback to ascending dist, fallback to sorting by position. - if (est != z.est) - { - return est > z.est; - } - if (dist != z.dist) - { - return dist < z.dist; - } - if (p.x != z.p.x) - { - return p.x < z.p.x; - } - return p.y < z.p.y; - } - - PathCoord p; // Map coords. - unsigned dist, est; // Distance so far and estimate to end. -}; -struct PathExploredTile -{ - PathExploredTile() : iteration(0xFFFF), dx(0), dy(0), dist(0), visited(false) {} - - uint16_t iteration; - int8_t dx, dy; // Offset from previous point in the route. - unsigned dist; // Shortest known distance to tile. - bool visited; -}; - -struct PathBlockingType -{ - uint32_t gameTime; - - PROPULSION_TYPE propulsion; - int owner; - FPATH_MOVETYPE moveType; -}; -/// Pathfinding blocking map -struct PathBlockingMap -{ - bool operator ==(PathBlockingType const &z) const - { - return type.gameTime == z.gameTime && - fpathIsEquivalentBlocking(type.propulsion, type.owner, type.moveType, - z.propulsion, z.owner, z.moveType); - } - - PathBlockingType type; - std::vector map; - std::vector dangerMap; // using threatBits -}; - -struct PathNonblockingArea -{ - PathNonblockingArea() {} - PathNonblockingArea(StructureBounds const &st) : x1(st.map.x), x2(st.map.x + st.size.x), y1(st.map.y), y2(st.map.y + st.size.y) {} - bool operator ==(PathNonblockingArea const &z) const - { - return x1 == z.x1 && x2 == z.x2 && y1 == z.y1 && y2 == z.y2; - } - bool operator !=(PathNonblockingArea const &z) const - { - return !(*this == z); - } - bool isNonblocking(int x, int y) const - { - return x >= x1 && x < x2 && y >= y1 && y < y2; - } - - int16_t x1 = 0; - int16_t x2 = 0; - int16_t y1 = 0; - int16_t y2 = 0; -}; - -// Data structures used for pathfinding, can contain cached results. -struct PathfindContext -{ - PathfindContext() : myGameTime(0), iteration(0), blockingMap(nullptr), pBlockingMap(nullptr) {} - bool isBlocked(int x, int y) const - { - if (dstIgnore.isNonblocking(x, y)) - { - return false; // The path is actually blocked here by a structure, but ignore it since it's where we want to go (or where we came from). - } - // Not sure whether the out-of-bounds check is needed, can only happen if pathfinding is started on a blocking tile (or off the map). - return x < 0 || y < 0 || x >= mapWidth || y >= mapHeight || pBlockingMap->map[x + y * mapWidth]; - } - bool isDangerous(int x, int y) const - { - return !pBlockingMap->dangerMap.empty() && pBlockingMap->dangerMap[x + y * mapWidth]; - } - bool matches(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_) const - { - // Must check myGameTime == blockingMap_->type.gameTime, otherwise blockingMap could be a deleted pointer which coincidentally compares equal to the valid pointer blockingMap_. - return myGameTime == blockingMap_->type.gameTime && blockingMap == blockingMap_ && tileS == tileS_ && dstIgnore == dstIgnore_; - } - void assign(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_) - { - blockingMap = blockingMap_; - pBlockingMap = blockingMap.get(); - tileS = tileS_; - dstIgnore = dstIgnore_; - myGameTime = blockingMap->type.gameTime; - nodes.clear(); - - // Make the iteration not match any value of iteration in map. - if (++iteration == 0xFFFF) - { - map.clear(); // There are no values of iteration guaranteed not to exist in map, so clear the map. - iteration = 0; - } - map.resize(static_cast(mapWidth) * static_cast(mapHeight)); // Allocate space for map, if needed. - } - - /// Start tile for pathfinding. - /// It is used for context caching and reusing existing contexts for other units. - /// (May be either source or target tile.) - PathCoord tileS; - uint32_t myGameTime; - - /** Counter to implement lazy deletion from map. - * - * @see fpathTableReset - */ - uint16_t iteration; - - std::vector nodes; ///< Edge of explored region of the map. - std::vector map; ///< Map, with paths leading back to tileS. - std::shared_ptr blockingMap; ///< Map of blocking tiles for the type of object which needs a path. - PathBlockingMap* pBlockingMap; ///< Direct pointer to blocking map. Using shared pointer in searching loop can hurt performance. - PathNonblockingArea dstIgnore; ///< Area of structure at destination which should be considered nonblocking. -}; - -/// Last recently used list of contexts. -static std::list fpathContexts; - -/// Lists of blocking maps from current tick. -static std::vector> fpathBlockingMaps; -/// Game time for all blocking maps in fpathBlockingMaps. -static uint32_t fpathCurrentGameTime; // Convert a direction into an offset // dir 0 => x = 0, y = -1 -static const Vector2i aDirOffset[] = +static constexpr Vector2i aDirOffset[] = { Vector2i(0, 1), Vector2i(-1, 1), @@ -235,10 +65,53 @@ static const Vector2i aDirOffset[] = Vector2i(1, 1), }; -void fpathHardTableReset() +static constexpr cost_t MaxPathCost = ~cost_t(0); + +bool isTileBlocked(const PathfindContext& context, int x, int y) { + if (context.dstIgnore.isNonblocking(x, y)) + { + return false; // The path is actually blocked here by a structure, but ignore it since it's where we want to go (or where we came from). + } + // Not sure whether the out-of-bounds check is needed, can only happen if pathfinding is started on a blocking tile (or off the map). + return x < 0 || y < 0 || x >= context.width || y >= context.height || context.blockingMap->map[x + y * context.width]; +} + +bool PathBlockingMap::operator ==(PathBlockingType const &z) const { - fpathContexts.clear(); - fpathBlockingMaps.clear(); + return type.gameTime == z.gameTime && + fpathIsEquivalentBlocking(type.propulsion, type.owner, static_cast(type.moveType), + z.propulsion, z.owner, static_cast(z.moveType)); +} + +PathCoord PathBlockingMap::worldToMap(int x, int y) const{ + return PathCoord(x >> tileShift, y >> tileShift); +} + +bool PathfindContext::matches(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_, bool reverse_) const +{ + // Must check myGameTime == blockingMap_->type.gameTime, otherwise blockingMap could be a deleted pointer which coincidentally compares equal to the valid pointer blockingMap_. + return myGameTime == blockingMap_->type.gameTime && blockingMap == blockingMap_ && tileS == tileS_ && dstIgnore == dstIgnore_ && reverse == reverse_; +} + +void PathfindContext::assign(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_, bool reverse_) +{ + ASSERT_OR_RETURN(, blockingMap_->width && blockingMap_->height, "Incorrect size of blocking map"); + blockingMap = blockingMap_; + tileS = tileS_; + dstIgnore = dstIgnore_; + myGameTime = blockingMap->type.gameTime; + reverse = reverse_; + nodes.clear(); + + // Make the iteration not match any value of iteration in map. + if (++iteration == 0xFFFF) + { + map.clear(); // There are no values of iteration guaranteed not to exist in map, so clear the map. + iteration = 0; + } + width = blockingMap_->width; + height = blockingMap_->height; + map.resize(static_cast(width) * static_cast(height)); // Allocate space for map, if needed. } /** Get the nearest entry in the open list @@ -271,15 +144,56 @@ static inline unsigned WZ_DECL_PURE fpathGoodEstimate(PathCoord s, PathCoord f) return iHypot((s.x - f.x) * 140, (s.y - f.y) * 140); } +/// Helper structure to extract blocking and cost information for PF wave propagation. +/// It must extract and cache data for direct access. +struct CostLayer { + CostLayer(const PathfindContext& pfc) : pfc(pfc) + { + assert(pfc.blockingMap); + pBlockingMap = pfc.blockingMap.get(); + } + + cost_t cost(int x, int y) const { + return isDangerous(x, y) ? 5 : 1; + } + + bool isBlocked(int x, int y) const + { + if (pfc.dstIgnore.isNonblocking(x, y)) + { + return false; // The path is actually blocked here by a structure, but ignore it since it's where we want to go (or where we came from). + } + // Not sure whether the out-of-bounds check is needed, can only happen if pathfinding is started on a blocking tile (or off the map). + return x < 0 || y < 0 || x >= mapWidth || y >= mapHeight || pBlockingMap->map[x + y * mapWidth]; + } + + bool isNonblocking(int x, int y) const { + return pfc.dstIgnore.isNonblocking(x, y); + } + + bool isDangerous(int x, int y) const + { + return !pBlockingMap->dangerMap.empty() && pBlockingMap->dangerMap[x + y * mapWidth]; + } + + const PathfindContext& pfc; + /// Direct pointer to blocking map. + PathBlockingMap* pBlockingMap = nullptr; +}; + /** Generate a new node */ -static inline void fpathNewNode(PathfindContext &context, unsigned estimateCost, PathCoord pos, unsigned prevDist, PathCoord prevPos) +template +bool fpathNewNode(PathfindContext &context, Predicate& predicate, + const CostLayer& costLayer, + PathCoord pos, cost_t prevDist, PathCoord prevPos) { - ASSERT_OR_RETURN(, (unsigned)pos.x < (unsigned)mapWidth && (unsigned)pos.y < (unsigned)mapHeight, "X (%d) or Y (%d) coordinate for path finding node is out of range!", pos.x, pos.y); + ASSERT_OR_RETURN(false, (unsigned)pos.x < (unsigned)mapWidth && (unsigned)pos.y < (unsigned)mapHeight, "X (%d) or Y (%d) coordinate for path finding node is out of range!", pos.x, pos.y); + unsigned estimateCost = predicate.estimateCost(pos); // Create the node. PathNode node; - unsigned costFactor = context.isDangerous(pos.x, pos.y) ? 5 : 1; + cost_t costFactor = costLayer.cost(pos.x, pos.y); node.p = pos; node.dist = prevDist + fpathEstimate(prevPos, pos) * costFactor; node.est = node.dist + estimateCost; @@ -292,7 +206,7 @@ static inline void fpathNewNode(PathfindContext &context, unsigned estimateCost, { if (expl.visited) { - return; // Already visited this tile. Do nothing. + return false; // Already visited this tile. Do nothing. } Vector2i deltaA = delta; Vector2i deltaB = Vector2i(expl.dx, expl.dy); @@ -306,8 +220,8 @@ static inline void fpathNewNode(PathfindContext &context, unsigned estimateCost, // +---+---+ // | A | B | // +---+---+ - unsigned distA = node.dist - (isDiagonal ? 198 : 140) * costFactor; // If isDiagonal, node is A and expl is B. - unsigned distB = expl.dist - (isDiagonal ? 140 : 198) * costFactor; + cost_t distA = node.dist - (isDiagonal ? 198 : 140) * costFactor; // If isDiagonal, node is A and expl is B. + cost_t distB = expl.dist - (isDiagonal ? 140 : 198) * costFactor; if (!isDiagonal) { std::swap(distA, distB); @@ -317,9 +231,9 @@ static inline void fpathNewNode(PathfindContext &context, unsigned estimateCost, if (gradientX > 0 && gradientX <= 98) // 98 = floor(140/√2), so gradientX <= 98 is needed so that gradientX < gradientY. { // The distance gradient is now known to be somewhere between the direction from A to P and the direction from B to P. - static const uint8_t gradYLookup[99] = {140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 139, 139, 139, 139, 139, 139, 139, 139, 139, 138, 138, 138, 138, 138, 138, 137, 137, 137, 137, 137, 136, 136, 136, 136, 135, 135, 135, 134, 134, 134, 134, 133, 133, 133, 132, 132, 132, 131, 131, 130, 130, 130, 129, 129, 128, 128, 127, 127, 126, 126, 126, 125, 125, 124, 123, 123, 122, 122, 121, 121, 120, 119, 119, 118, 118, 117, 116, 116, 115, 114, 113, 113, 112, 111, 110, 110, 109, 108, 107, 106, 106, 105, 104, 103, 102, 101, 100}; + static constexpr uint8_t gradYLookup[99] = {140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 139, 139, 139, 139, 139, 139, 139, 139, 139, 138, 138, 138, 138, 138, 138, 137, 137, 137, 137, 137, 136, 136, 136, 136, 135, 135, 135, 134, 134, 134, 134, 133, 133, 133, 132, 132, 132, 131, 131, 130, 130, 130, 129, 129, 128, 128, 127, 127, 126, 126, 126, 125, 125, 124, 123, 123, 122, 122, 121, 121, 120, 119, 119, 118, 118, 117, 116, 116, 115, 114, 113, 113, 112, 111, 110, 110, 109, 108, 107, 106, 106, 105, 104, 103, 102, 101, 100}; int gradientY = gradYLookup[gradientX]; // = sqrt(140² - gradientX²), rounded to nearest integer - unsigned distP = gradientY * costFactor + distB; + cost_t distP = gradientY * costFactor + distB; node.est -= node.dist - distP; node.dist = distP; delta = (deltaA * gradientX + deltaB * (gradientY - gradientX)) / gradientY; @@ -327,7 +241,7 @@ static inline void fpathNewNode(PathfindContext &context, unsigned estimateCost, } if (expl.dist <= node.dist) { - return; // A different path to this tile is shorter. + return false; // A different path to this tile is shorter. } } @@ -341,6 +255,7 @@ static inline void fpathNewNode(PathfindContext &context, unsigned estimateCost, // Add the node to the node heap. context.nodes.push_back(node); // Add the new node to nodes. std::push_heap(context.nodes.begin(), context.nodes.end()); // Move the new node to the right place in the heap. + return true; } /// Recalculates estimates to new tileF tile. @@ -362,8 +277,9 @@ struct NearestSearchPredicate { PathCoord goal; /// Nearest coordinates of the wave to the target tile. PathCoord nearestCoord {0, 0}; + /// Nearest distance to the target. - unsigned nearestDist = 0xFFFFFFFF; + cost_t nearestDist = MaxPathCost; NearestSearchPredicate(const PathCoord& goal) : goal(goal) { } @@ -388,7 +304,17 @@ struct NearestSearchPredicate { void clear() { nearestCoord = {0, 0}; - nearestDist = 0xFFFFFFFF; + nearestDist = MaxPathCost; + } +}; + +struct ExplorationReport { + bool success = false; + size_t tilesExplored = 0; + cost_t cost = 0; + + operator bool() const { + return success; } }; @@ -396,21 +322,27 @@ struct NearestSearchPredicate { /// Target is checked using predicate object. /// @returns true if search wave has reached the goal or false if /// the wave has collapsed before reaching the goal. -template -static bool fpathAStarExplore(PathfindContext &context, Predicate& predicate) +template +static ExplorationReport fpathAStarExplore(PathfindContext &context, Predicate& predicate, CostLayer& costLayer) { + ExplorationReport report; constexpr int adjacency = 8; while (!context.nodes.empty()) { PathNode node = fpathTakeNode(context.nodes); + report.tilesExplored++; + report.cost = node.dist; + if (context.map[node.p.x + node.p.y * mapWidth].visited) { continue; // Already been here. } context.map[node.p.x + node.p.y * mapWidth].visited = true; - if (predicate.isGoal(node)) - return true; + if (predicate.isGoal(node)) { + report.success = true; + break; + } /* 5 6 7 @@ -427,11 +359,11 @@ static bool fpathAStarExplore(PathfindContext &context, Predicate& predicate) for (unsigned dir = 0; dir < adjacency; ++dir) { int x = node.p.x + aDirOffset[dir].x; int y = node.p.y + aDirOffset[dir].y; - blocking[dir] = context.isBlocked(x, y); - ignoreBlocking[dir] = context.dstIgnore.isNonblocking(x, y); + blocking[dir] = costLayer.isBlocked(x, y); + ignoreBlocking[dir] = costLayer.isNonblocking(x, y); } - bool ignoreCenter = context.dstIgnore.isNonblocking(node.p.x, node.p.y); + bool ignoreCenter = costLayer.isNonblocking(node.p.x, node.p.y); // loop through possible moves in 8 directions to find a valid move for (unsigned dir = 0; dir < adjacency; ++dir) @@ -454,23 +386,13 @@ static bool fpathAStarExplore(PathfindContext &context, Predicate& predicate) int y = node.p.y + aDirOffset[dir].y; PathCoord newPos(x, y); - unsigned estimateCost = predicate.estimateCost(newPos); + // Now insert the point into the appropriate list, if not already visited. - fpathNewNode(context, estimateCost, newPos, node.dist, node.p); + fpathNewNode(context, predicate, costLayer, newPos, node.dist, node.p); } } - return false; -} - -static void fpathInitContext(PathfindContext &context, std::shared_ptr &blockingMap, PathCoord tileS, PathCoord tileRealS, PathCoord tileF, PathNonblockingArea dstIgnore) -{ - context.assign(blockingMap, tileS, dstIgnore); - - // Add the start point to the open list - unsigned estimateCost = fpathGoodEstimate(tileRealS, tileF); - fpathNewNode(context, estimateCost, tileRealS, 0, tileRealS); - ASSERT(!context.nodes.empty(), "fpathNewNode failed to add node."); + return report; } // Traces path from search tree. @@ -492,11 +414,11 @@ static ASR_RETVAL fpathTracePath(const PathfindContext& context, PathCoord src, Vector2i mapP = map_coord(newP); int xSide = newP.x - world_coord(mapP.x) > TILE_UNITS / 2 ? 1 : -1; // 1 if newP is on right-hand side of the tile, or -1 if newP is on the left-hand side of the tile. int ySide = newP.y - world_coord(mapP.y) > TILE_UNITS / 2 ? 1 : -1; // 1 if newP is on bottom side of the tile, or -1 if newP is on the top side of the tile. - if (context.isBlocked(mapP.x + xSide, mapP.y)) + if (isTileBlocked(context, mapP.x + xSide, mapP.y)) { newP.x = world_coord(mapP.x) + TILE_UNITS / 2; // Point too close to a blocking tile on left or right side, so move the point to the middle. } - if (context.isBlocked(mapP.x, mapP.y + ySide)) + if (isTileBlocked(context, mapP.x, mapP.y + ySide)) { newP.y = world_coord(mapP.y) + TILE_UNITS / 2; // Point too close to a blocking tile on rop or bottom side, so move the point to the middle. } @@ -508,61 +430,65 @@ static ASR_RETVAL fpathTracePath(const PathfindContext& context, PathCoord src, return retval; } -#if DEBUG -extern uint32_t selectedPlayer; -#endif - -ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) +ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, + MOVE_CONTROL *psMove, PATHJOB *psJob) { - ASR_RETVAL retval = ASR_OK; + ASSERT_OR_RETURN(ASR_FAILED, psMove, "Null psMove"); + ASSERT_OR_RETURN(ASR_FAILED, psJob, "Null psMove"); - bool mustReverse = true; + ASR_RETVAL retval = ASR_OK; - const PathCoord tileOrig(map_coord(psJob->origX), map_coord(psJob->origY)); - const PathCoord tileDest(map_coord(psJob->destX), map_coord(psJob->destY)); - const PathNonblockingArea dstIgnore(psJob->dstStructure); + bool mustReverse = false; -#if DEBUG - const DROID *psDroid = IdToDroid(psJob->droidID, selectedPlayer); - const bool isDroidSelected = (psDroid && psDroid->selected); -#endif + const PathCoord tileOrig = psJob->blockingMap->worldToMap(psJob->origX, psJob->origY); + const PathCoord tileDest = psJob->blockingMap->worldToMap(psJob->destX, psJob->destY); + + if (psJob->blockingMap->isBlocked(tileOrig.x, tileOrig.y)) { + debug(LOG_NEVER, "Initial tile blocked (%d;%d)", tileOrig.x, tileOrig.y); + } + if (psJob->blockingMap->isBlocked(tileDest.x, tileDest.y)) { + debug(LOG_NEVER, "Destination tile blocked (%d;%d)", tileOrig.x, tileOrig.y); + } + const PathNonblockingArea dstIgnore(psJob->dstStructure); - NearestSearchPredicate pred(tileDest); + NearestSearchPredicate pred(tileOrig); - PathCoord endCoord; // Either nearest coord (mustReverse = true) or orig (mustReverse = false). + PathCoord endCoord; + // Caching reverse searches. std::list::iterator contextIterator = fpathContexts.begin(); for (; contextIterator != fpathContexts.end(); ++contextIterator) { PathfindContext& pfContext = *contextIterator; - if (!pfContext.matches(psJob->blockingMap, tileDest, dstIgnore)) + if (!pfContext.matches(psJob->blockingMap, tileDest, dstIgnore, /*reverse*/true)) { // This context is not for the same droid type and same destination. continue; } + const PathExploredTile& pt = pfContext.tile(tileOrig); // We have tried going to tileDest before. - if (pfContext.map[tileOrig.x + tileOrig.y * mapWidth].iteration == pfContext.iteration - && pfContext.map[tileOrig.x + tileOrig.y * mapWidth].visited) + if (pfContext.isTileVisited(pt)) { // Already know the path from orig to dest. endCoord = tileOrig; } else if (pfContext.nodes.empty()) { // Wave has already collapsed. Consequent attempt to search will exit immediately. + // We can be here only if there is literally no path existing. continue; } else { + CostLayer costLayer(pfContext); // Need to find the path from orig to dest, continue previous exploration. - fpathAStarReestimate(pfContext, tileOrig); + fpathAStarReestimate(pfContext, pred.goal); pred.clear(); - if (fpathAStarExplore(pfContext, pred)) { + ExplorationReport report = fpathAStarExplore(pfContext, pred, costLayer); + if (report) { endCoord = pred.nearestCoord; - mustReverse = false; // We have the path from the nearest reachable tile to dest, to orig. - break; // Found the path! Don't search more contexts. - } else { - syncDebug("fpathAStarRoute (%d,%d) to (%d,%d) - wave collapsed. Nearest=%d", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y, pred.nearestDist); + // Found the path! Don't search more contexts. + break; } } } @@ -579,24 +505,32 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) // Init a new context, overwriting the oldest one if we are caching too many. // We will be searching from orig to dest, since we don't know where the nearest reachable tile to dest is. - fpathInitContext(pfContext, psJob->blockingMap, tileOrig, tileOrig, tileDest, dstIgnore); - + pfContext.assign(psJob->blockingMap, tileDest, dstIgnore, true); pred.clear(); - fpathAStarExplore(pfContext, pred); + + CostLayer costLayer(pfContext); + // Add the start point to the open list + bool started = fpathNewNode(pfContext, pred, costLayer, tileDest, 0, tileDest); + ASSERT(started, "fpathNewNode failed to add node."); + + ExplorationReport report = fpathAStarExplore(pfContext, pred, costLayer); + if (!report) { + debug(LOG_NEVER, "Failed to find path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); + } endCoord = pred.nearestCoord; } PathfindContext &context = *contextIterator; // return the nearest route if no actual route was found - if (endCoord != tileDest) + if (endCoord != pred.goal) { retval = ASR_NEAREST; } static std::vector path; // Declared static to save allocations. - ASR_RETVAL traceRet = fpathTracePath(context, endCoord, tileOrig, path); + ASR_RETVAL traceRet = fpathTracePath(context, endCoord, tileDest, path); if (traceRet != ASR_OK) return traceRet; @@ -632,12 +566,6 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) { // Copy the list, in reverse. std::copy(path.rbegin(), path.rend(), psMove->asPath.data()); - - if (!context.isBlocked(tileOrig.x, tileOrig.y)) // If blocked, searching from tileDest to tileOrig wouldn't find the tileOrig tile. - { - // Next time, search starting from nearest reachable tile to the destination. - fpathInitContext(context, psJob->blockingMap, tileDest, pred.nearestCoord, tileOrig, dstIgnore); - } } else { @@ -645,32 +573,82 @@ ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob) std::copy(path.begin(), path.end(), psMove->asPath.data()); } + psMove->destination = psMove->asPath[path.size() - 1]; + // Move context to beginning of last recently used list. if (contextIterator != fpathContexts.begin()) // Not sure whether or not the splice is a safe noop, if equal. { fpathContexts.splice(fpathContexts.begin(), fpathContexts, contextIterator); } - psMove->destination = psMove->asPath[path.size() - 1]; - return retval; } -void fpathSetBlockingMap(PATHJOB *psJob) +void PathMapCache::clear() +{ + fpathBlockingMaps.clear(); +} + +struct I32Checksum { + uint32_t factor = 0; + uint32_t checksum = 0; + + I32Checksum& operator += (bool value) { + checksum ^= value * (factor = 3 * factor + 1); + return *this; + } + + operator uint32_t() const { + return checksum; + } +}; + +uint32_t bitmapChecksum(const std::vector& map) { + I32Checksum checksum; + for (auto v: map) + checksum += v; + return checksum; +} + +void fillBlockingMap(PathBlockingMap& blockMap, PathBlockingType type) { + // blockMap now points to an empty map with no data. Fill the map. + blockMap.type = type; + std::vector &map = blockMap.map; + map.resize(static_cast(mapWidth) * static_cast(mapHeight)); + auto moveType = static_cast(type.moveType); + for (int y = 0; y < mapHeight; ++y) { + for (int x = 0; x < mapWidth; ++x) + map[x + y * mapWidth] = fpathBaseBlockingTile(x, y, type.propulsion, type.owner, moveType); + } + if (!isHumanPlayer(type.owner) && type.moveType == FMT_MOVE) + { + std::vector &dangerMap = blockMap.dangerMap; + dangerMap.resize(static_cast(mapWidth) * static_cast(mapHeight)); + for (int y = 0; y < mapHeight; ++y) { + for (int x = 0; x < mapWidth; ++x) + dangerMap[x + y * mapWidth] = auxTile(x, y, type.owner) & AUXBITS_THREAT; + } + } + blockMap.width = mapWidth; + blockMap.height = mapHeight; + blockMap.tileShift = TILE_SHIFT; +} + +void PathMapCache::assignBlockingMap(PATHJOB& psJob) { if (fpathCurrentGameTime != gameTime) { // New tick, remove maps which are no longer needed. fpathCurrentGameTime = gameTime; - fpathBlockingMaps.clear(); + clear(); } // Figure out which map we are looking for. PathBlockingType type; type.gameTime = gameTime; - type.propulsion = psJob->propulsion; - type.owner = psJob->owner; - type.moveType = psJob->moveType; + type.propulsion = psJob.propulsion; + type.owner = psJob.owner; + type.moveType = psJob.moveType; // Find the map. auto i = std::find_if(fpathBlockingMaps.begin(), fpathBlockingMaps.end(), [&](std::shared_ptr const &ptr) { @@ -679,39 +657,17 @@ void fpathSetBlockingMap(PATHJOB *psJob) if (i == fpathBlockingMaps.end()) { // Didn't find the map, so i does not point to a map. - PathBlockingMap *blockMap = new PathBlockingMap(); - fpathBlockingMaps.emplace_back(blockMap); - - // blockMap now points to an empty map with no data. Fill the map. - blockMap->type = type; - std::vector &map = blockMap->map; - map.resize(static_cast(mapWidth) * static_cast(mapHeight)); - uint32_t checksumMap = 0, checksumDangerMap = 0, factor = 0; - for (int y = 0; y < mapHeight; ++y) - for (int x = 0; x < mapWidth; ++x) - { - map[x + y * mapWidth] = fpathBaseBlockingTile(x, y, type.propulsion, type.owner, type.moveType); - checksumMap ^= map[x + y * mapWidth] * (factor = 3 * factor + 1); - } - if (!isHumanPlayer(type.owner) && type.moveType == FMT_MOVE) - { - std::vector &dangerMap = blockMap->dangerMap; - dangerMap.resize(static_cast(mapWidth) * static_cast(mapHeight)); - for (int y = 0; y < mapHeight; ++y) - for (int x = 0; x < mapWidth; ++x) - { - dangerMap[x + y * mapWidth] = auxTile(x, y, type.owner) & AUXBITS_THREAT; - checksumDangerMap ^= dangerMap[x + y * mapWidth] * (factor = 3 * factor + 1); - } - } - syncDebug("blockingMap(%d,%d,%d,%d) = %08X %08X", gameTime, psJob->propulsion, psJob->owner, psJob->moveType, checksumMap, checksumDangerMap); - - psJob->blockingMap = fpathBlockingMaps.back(); + auto blockMap = std::make_shared(); + fpathBlockingMaps.push_back(blockMap); + fillBlockingMap(*blockMap, type); + debug(LOG_NEVER, "blockingMap(%d,%d,%d,%d) = %08X %08X", gameTime, psJob.propulsion, psJob.owner, psJob.moveType, + bitmapChecksum(blockMap->map), bitmapChecksum(blockMap->dangerMap)); + psJob.blockingMap = blockMap; } else { - syncDebug("blockingMap(%d,%d,%d,%d) = cached", gameTime, psJob->propulsion, psJob->owner, psJob->moveType); - - psJob->blockingMap = *i; + debug(LOG_NEVER, "blockingMap(%d,%d,%d,%d) = cached", gameTime, psJob.propulsion, psJob.owner, psJob.moveType); + ASSERT_OR_RETURN(, *i != nullptr, "Found null map pointer in cache"); + psJob.blockingMap = *i; } } diff --git a/src/astar.h b/src/astar.h index e0ecb599857..ca8913d5332 100644 --- a/src/astar.h +++ b/src/astar.h @@ -21,7 +21,11 @@ #ifndef __INCLUDED_SRC_ASTART_H__ #define __INCLUDED_SRC_ASTART_H__ -#include "fpath.h" +#include +#include +#include + +#include "baseobject.h" /** return codes for astar * @@ -34,22 +38,192 @@ enum ASR_RETVAL ASR_NEAREST, ///< found a partial route to a nearby position }; -/** Use the A* algorithm to find a path +/// A coordinate. +struct PathCoord +{ + PathCoord(): x(0), y(0) {} + PathCoord(int16_t x_, int16_t y_) : x(x_), y(y_) {} + bool operator ==(PathCoord const &z) const + { + return x == z.x && y == z.y; + } + bool operator !=(PathCoord const &z) const + { + return !(*this == z); + } + + int16_t x, y; +}; + +/// Common type for all pathfinding costs. +using cost_t = unsigned; + +/** The structure to store a node of the route in node table * * @ingroup pathfinding */ -ASR_RETVAL fpathAStarRoute(MOVE_CONTROL *psMove, PATHJOB *psJob); +struct PathNode +{ + bool operator <(PathNode const &z) const + { + // Sort descending est, fallback to ascending dist, fallback to sorting by position. + if (est != z.est) + { + return est > z.est; + } + if (dist != z.dist) + { + return dist < z.dist; + } + if (p.x != z.p.x) + { + return p.x < z.p.x; + } + return p.y < z.p.y; + } + + PathCoord p; // Map coords. + cost_t dist, est; // Distance so far and estimate to end. +}; -/// Call from main thread. -/// Sets psJob->blockingMap for later use by pathfinding thread, generating the required map if not already generated. -void fpathSetBlockingMap(PATHJOB *psJob); +struct PathExploredTile +{ + PathExploredTile() : iteration(0xFFFF), dx(0), dy(0), dist(0), visited(false) {} -/** Clean up the path finding node table. - * - * @note Call this on shutdown to prevent memory from leaking, or if loading/saving, to prevent stale data from being reused. + uint16_t iteration; + int8_t dx, dy; // Offset from previous point in the route. + cost_t dist; // Shortest known distance to tile. + bool visited; +}; + +struct PathBlockingType +{ + uint32_t gameTime; + + PROPULSION_TYPE propulsion; + int owner; + /// It is the same as FPATH_MOVETYPE, but can have a wider meaning. + int moveType; +}; + +/// Blocking map for PF wave exploration. +/// This map is extracted from game map and used in PF query. +struct PathBlockingMap +{ + PathBlockingType type; + std::vector map; + std::vector dangerMap; + int width = 0; + int height = 0; + int tileShift = 0; + + PathCoord worldToMap(int x, int y) const; + bool operator ==(PathBlockingType const &z) const; + bool isBlocked(int x, int y) const { + if (x < 0 || y < 0 || x >= width || y >= width) + return true; + return map[x + y*width]; + } +}; + +struct PathNonblockingArea +{ + PathNonblockingArea() {} + PathNonblockingArea(StructureBounds const &st) : x1(st.map.x), x2(st.map.x + st.size.x), y1(st.map.y), y2(st.map.y + st.size.y) {} + + bool operator ==(PathNonblockingArea const &z) const + { + return x1 == z.x1 && x2 == z.x2 && y1 == z.y1 && y2 == z.y2; + } + + bool operator !=(PathNonblockingArea const &z) const + { + return !(*this == z); + } + + bool isNonblocking(int x, int y) const + { + return x >= x1 && x < x2 && y >= y1 && y < y2; + } + + int16_t x1 = 0; + int16_t x2 = 0; + int16_t y1 = 0; + int16_t y2 = 0; +}; + +struct PATHJOB; +/** + * A cache of blocking maps for pathfinding system. + */ +class PathMapCache { +public: + void clear(); + + /// Assigns blocking map for PF job. + /// It is expected to be called from the main thread. + void assignBlockingMap(PATHJOB& psJob); + +protected: + /// Lists of blocking maps from current tick. + std::vector> fpathBlockingMaps; + /// Game time for all blocking maps in fpathBlockingMaps. + uint32_t fpathCurrentGameTime; +}; + +/// Data structures used for pathfinding, can contain cached results. +/// Separate context can +struct PathfindContext +{ + /// Test if this context can be reused for new search. + bool matches(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_, bool reverse) const; + + /// Assign new search configuration to a context. + void assign(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_, bool reverse); + + const PathExploredTile& tile(const PathCoord& pc) const { + return map[pc.x + width * pc.y]; + } + + PathExploredTile& tile(const PathCoord& pc) { + return map[pc.x + width * pc.y]; + } + + bool isTileExplored(const PathExploredTile& tile) const { + return tile.iteration == iteration; + } + + bool isTileVisited(const PathExploredTile& tile) const { + return tile.iteration == iteration && tile.visited; + } + /// Start tile for pathfinding. + /// It is used for context caching and reusing existing contexts for other units. + /// (May be either source or target tile.) + PathCoord tileS; + uint32_t myGameTime = 0; + + /** Counter to implement lazy deletion from map. + * + * @see fpathTableReset + */ + uint16_t iteration = 0; + + /// Local geometry of a map. + /// It can differ from dimensions of global map. + int width = 0, height = 0; + + /// This is the context for reverse search. + bool reverse = true; + std::vector nodes; ///< Edge of explored region of the map. + std::vector map; ///< Map, with paths leading back to tileS. + std::shared_ptr blockingMap; ///< Map of blocking tiles for the type of object which needs a path. + PathNonblockingArea dstIgnore; ///< Area of structure at destination which should be considered nonblocking. +}; + +/** Use the A* algorithm to find a path * * @ingroup pathfinding */ -void fpathHardTableReset(); +ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, MOVE_CONTROL *psMove, PATHJOB *psJob); #endif // __INCLUDED_SRC_ASTART_H__ diff --git a/src/fpath.cpp b/src/fpath.cpp index 0c6aa7d32bc..9c8864c14fe 100644 --- a/src/fpath.cpp +++ b/src/fpath.cpp @@ -42,6 +42,9 @@ // If the path finding system is shutdown or not static volatile bool fpathQuit = false; +// Run PF tasks in separate thread. +// Can be switch off to simplify debugging of PF system. +constexpr bool fpathAsyncMode = false; /* Beware: Enabling this will cause significant slow-down. */ #undef DEBUG_MAP @@ -67,8 +70,12 @@ static bool waitingForResult = false; static uint32_t waitingForResultId; static WZ_SEMAPHORE *waitingForResultSemaphore = nullptr; +/// Last recently used list of contexts. +static std::list fpathContexts; + static PATHRESULT fpathExecute(PATHJOB psJob); +static PathMapCache pfMapCache; /** This runs in a separate thread */ static int fpathThreadFunc(void *) @@ -144,7 +151,9 @@ void fpathShutdown() wzSemaphoreDestroy(waitingForResultSemaphore); waitingForResultSemaphore = nullptr; } - fpathHardTableReset(); + + fpathContexts.clear(); + pfMapCache.clear(); } @@ -339,7 +348,7 @@ static FPATH_RETVAL fpathRoute(MOVE_CONTROL *psMove, unsigned id, int startX, in } // Check if waiting for a result - while (psMove->Status == MOVEWAITROUTE) + if (psMove->Status == MOVEWAITROUTE) { objTrace(id, "Checking if we have a path yet"); @@ -362,16 +371,13 @@ static FPATH_RETVAL fpathRoute(MOVE_CONTROL *psMove, unsigned id, int startX, in objTrace(id, "Got a path to (%d, %d)! Length=%d Retval=%d", psMove->destination.x, psMove->destination.y, (int)psMove->asPath.size(), (int)retval); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = %d, path[%d] = %08X->(%d, %d)", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner, retval, (int)psMove->asPath.size(), ~crcSumVector2i(0, psMove->asPath.data(), psMove->asPath.size()), psMove->destination.x, psMove->destination.y); - - if (!correctDestination) + // There was some probability that we get result from older pathfinding request. + // So we must check that we've got the path to the correct goal. + if (correctDestination) { - goto queuePathfinding; // Seems we got the result of an old pathfinding job for this droid, so need to pathfind again. + return retval; } - - return retval; } -queuePathfinding: - // We were not waiting for a result, and found no trivial path, so create new job and start waiting PATHJOB job; job.origX = startX; @@ -386,28 +392,34 @@ static FPATH_RETVAL fpathRoute(MOVE_CONTROL *psMove, unsigned id, int startX, in job.owner = owner; job.acceptNearest = acceptNearest; job.deleted = false; - fpathSetBlockingMap(&job); + pfMapCache.assignBlockingMap(job); - debug(LOG_NEVER, "starting new job for droid %d 0x%x", id, id); + debug(LOG_NEVER, "starting new job for droid %d 0x%x (%d;%d) to (%d;%d)", id, id, startX, startY, tX, tY); // Clear any results or jobs waiting already. It is a vital assumption that there is only one // job or result for each droid in the system at any time. fpathRemoveDroidData(id); - packagedPathJob task([job]() { return fpathExecute(job); }); - pathResults[id] = task.get_future(); - - // Add to end of list - wzMutexLock(fpathMutex); - bool isFirstJob = pathJobs.empty(); - pathJobs.push_back(std::move(task)); - wzMutexUnlock(fpathMutex); + if (fpathAsyncMode) { + wz::packaged_task task([job]() { return fpathExecute(job); }); + pathResults[id] = task.get_future(); + // Add to end of list + wzMutexLock(fpathMutex); + bool isFirstJob = pathJobs.empty(); + pathJobs.push_back(std::move(task)); + wzMutexUnlock(fpathMutex); - if (isFirstJob) - { - wzSemaphorePost(fpathSemaphore); // Wake up processing thread. + if (isFirstJob) + { + wzSemaphorePost(fpathSemaphore); // Wake up processing thread. + } + objTrace(id, "Queued up a path-finding request to (%d, %d), at least %d items earlier in queue", tX, tY, isFirstJob); + } else { + // This assignment produces much simpler call stack and easier to debug. + std::promise result; + pathResults[id] = result.get_future(); + result.set_value(fpathExecute(job)); } - objTrace(id, "Queued up a path-finding request to (%d, %d), at least %d items earlier in queue", tX, tY, isFirstJob); syncDebug("fpathRoute(..., %d, %d, %d, %d, %d, %d, %d, %d, %d) = FPR_WAIT", id, startX, startY, tX, tY, propulsionType, droidType, moveType, owner); return FPR_WAIT; // wait while polling result queue } @@ -465,7 +477,7 @@ PATHRESULT fpathExecute(PATHJOB job) result.retval = FPR_FAILED; result.originalDest = Vector2i(job.destX, job.destY); - ASR_RETVAL retval = fpathAStarRoute(&result.sMove, &job); + ASR_RETVAL retval = fpathAStarRoute(fpathContexts, &result.sMove, &job); ASSERT(retval != ASR_OK || result.sMove.asPath.size() > 0, "Ok result but no path in result"); switch (retval) From 927d15b7d755a0a9ea9e46f14dd13459b6187e7d Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Tue, 25 Jul 2023 21:47:23 +0300 Subject: [PATCH 07/17] Eliminated some reliance of AStar on global geometry of the map --- src/astar.cpp | 33 ++++++++++++++++++--------------- src/astar.h | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index 96ae4ab5547..a245f2b3f3e 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -87,6 +87,10 @@ PathCoord PathBlockingMap::worldToMap(int x, int y) const{ return PathCoord(x >> tileShift, y >> tileShift); } +PathCoord PathBlockingMap::mapToWorld(int x, int y) const { + return PathCoord(x << tileShift, y << tileShift); +} + bool PathfindContext::matches(std::shared_ptr &blockingMap_, PathCoord tileS_, PathNonblockingArea dstIgnore_, bool reverse_) const { // Must check myGameTime == blockingMap_->type.gameTime, otherwise blockingMap could be a deleted pointer which coincidentally compares equal to the valid pointer blockingMap_. @@ -164,7 +168,7 @@ struct CostLayer { return false; // The path is actually blocked here by a structure, but ignore it since it's where we want to go (or where we came from). } // Not sure whether the out-of-bounds check is needed, can only happen if pathfinding is started on a blocking tile (or off the map). - return x < 0 || y < 0 || x >= mapWidth || y >= mapHeight || pBlockingMap->map[x + y * mapWidth]; + return x < 0 || y < 0 || x >= mapWidth || y >= mapHeight || pBlockingMap->isBlocked(x, y); } bool isNonblocking(int x, int y) const { @@ -173,7 +177,7 @@ struct CostLayer { bool isDangerous(int x, int y) const { - return !pBlockingMap->dangerMap.empty() && pBlockingMap->dangerMap[x + y * mapWidth]; + return !pBlockingMap->dangerMap.empty() && pBlockingMap->isDangerous(x, y); } const PathfindContext& pfc; @@ -188,7 +192,7 @@ bool fpathNewNode(PathfindContext &context, Predicate& predicate, const CostLayer& costLayer, PathCoord pos, cost_t prevDist, PathCoord prevPos) { - ASSERT_OR_RETURN(false, (unsigned)pos.x < (unsigned)mapWidth && (unsigned)pos.y < (unsigned)mapHeight, "X (%d) or Y (%d) coordinate for path finding node is out of range!", pos.x, pos.y); + ASSERT_OR_RETURN(false, (unsigned)pos.x < (unsigned)context.width && (unsigned)pos.y < (unsigned)context.height, "X (%d) or Y (%d) coordinate for path finding node is out of range!", pos.x, pos.y); unsigned estimateCost = predicate.estimateCost(pos); // Create the node. @@ -201,7 +205,7 @@ bool fpathNewNode(PathfindContext &context, Predicate& predicate, Vector2i delta = Vector2i(pos.x - prevPos.x, pos.y - prevPos.y) * 64; bool isDiagonal = delta.x && delta.y; - PathExploredTile &expl = context.map[pos.x + pos.y * mapWidth]; + PathExploredTile &expl = context.map[pos.x + pos.y * context.width]; if (expl.iteration == context.iteration) { if (expl.visited) @@ -333,11 +337,10 @@ static ExplorationReport fpathAStarExplore(PathfindContext &context, Predicate& report.tilesExplored++; report.cost = node.dist; - if (context.map[node.p.x + node.p.y * mapWidth].visited) - { - continue; // Already been here. - } - context.map[node.p.x + node.p.y * mapWidth].visited = true; + PathExploredTile& tile = context.tile(node.p); + if (context.isTileVisited(tile)) + continue; + tile.visited = true; if (predicate.isGoal(node)) { report.success = true; @@ -353,7 +356,7 @@ static ExplorationReport fpathAStarExplore(PathfindContext &context, Predicate& odd:orthogonal-adjacent tiles even:non-orthogonal-adjacent tiles */ - // Cached state from blocking map. + // Cache adjacent states from blocking map. Saves some cycles for diagonal checks for corners. bool blocking[adjacency]; bool ignoreBlocking[adjacency]; for (unsigned dir = 0; dir < adjacency; ++dir) { @@ -395,9 +398,9 @@ static ExplorationReport fpathAStarExplore(PathfindContext &context, Predicate& return report; } -// Traces path from search tree. -// @param src - starting point of a search -// @param dst - final point, when tracing stops. +/// Traces path from search tree. +/// @param src - starting point of a search +/// @param dst - final point, when tracing stops. static ASR_RETVAL fpathTracePath(const PathfindContext& context, PathCoord src, PathCoord dst, std::vector& path) { ASR_RETVAL retval = ASR_OK; path.clear(); @@ -620,10 +623,10 @@ void fillBlockingMap(PathBlockingMap& blockMap, PathBlockingType type) { for (int x = 0; x < mapWidth; ++x) map[x + y * mapWidth] = fpathBaseBlockingTile(x, y, type.propulsion, type.owner, moveType); } + std::vector &dangerMap = blockMap.dangerMap; + dangerMap.resize(static_cast(mapWidth) * static_cast(mapHeight)); if (!isHumanPlayer(type.owner) && type.moveType == FMT_MOVE) { - std::vector &dangerMap = blockMap.dangerMap; - dangerMap.resize(static_cast(mapWidth) * static_cast(mapHeight)); for (int y = 0; y < mapHeight; ++y) { for (int x = 0; x < mapWidth; ++x) dangerMap[x + y * mapWidth] = auxTile(x, y, type.owner) & AUXBITS_THREAT; diff --git a/src/astar.h b/src/astar.h index ca8913d5332..e12e940d994 100644 --- a/src/astar.h +++ b/src/astar.h @@ -118,12 +118,26 @@ struct PathBlockingMap int tileShift = 0; PathCoord worldToMap(int x, int y) const; + + PathCoord mapToWorld(int x, int y) const; + + /// Returns size of map tile in world coordinates. + int tileSize() const { + return 1<= width || y >= width) + if (x < 0 || y < 0 || x >= width || y >= height) return true; return map[x + y*width]; } + + bool isDangerous(int x, int y) const { + if (x < 0 || y < 0 || x >= width || y >= height) + return true; + return dangerMap[x + y*width]; + } }; struct PathNonblockingArea @@ -220,6 +234,9 @@ struct PathfindContext PathNonblockingArea dstIgnore; ///< Area of structure at destination which should be considered nonblocking. }; +/** Fills in blocking map according to requested blocking type. */ +void fillBlockingMap(PathBlockingMap& blockMap, PathBlockingType type); + /** Use the A* algorithm to find a path * * @ingroup pathfinding From 0f33195bb74bae21e1328d0958a60f2e28218cd9 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sat, 29 Jul 2023 18:41:33 +0300 Subject: [PATCH 08/17] Astar: workaround for the case when starting position is blocked --- src/astar.cpp | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index a245f2b3f3e..8c857c9320a 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -185,6 +185,44 @@ struct CostLayer { PathBlockingMap* pBlockingMap = nullptr; }; +struct IgnoreGoalCostLayer { + IgnoreGoalCostLayer(const PathfindContext& pfc, PathCoord goal) : pfc(pfc) + { + assert(pfc.blockingMap); + pBlockingMap = pfc.blockingMap.get(); + } + + cost_t cost(int x, int y) const { + return isDangerous(x, y) ? 5 : 1; + } + + bool isBlocked(int x, int y) const + { + if (pfc.dstIgnore.isNonblocking(x, y)) + { + return false; // The path is actually blocked here by a structure, but ignore it since it's where we want to go (or where we came from). + } + if (goal.x == x && goal.y == y) + return false; + // Not sure whether the out-of-bounds check is needed, can only happen if pathfinding is started on a blocking tile (or off the map). + return x < 0 || y < 0 || x >= mapWidth || y >= mapHeight || pBlockingMap->isBlocked(x, y); + } + + bool isNonblocking(int x, int y) const { + return pfc.dstIgnore.isNonblocking(x, y); + } + + bool isDangerous(int x, int y) const + { + return !pBlockingMap->dangerMap.empty() && pBlockingMap->isDangerous(x, y); + } + + const PathfindContext& pfc; + PathCoord goal; + /// Direct pointer to blocking map. + PathBlockingMap* pBlockingMap = nullptr; +}; + /** Generate a new node */ template @@ -450,8 +488,9 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, debug(LOG_NEVER, "Initial tile blocked (%d;%d)", tileOrig.x, tileOrig.y); } if (psJob->blockingMap->isBlocked(tileDest.x, tileDest.y)) { - debug(LOG_NEVER, "Destination tile blocked (%d;%d)", tileOrig.x, tileOrig.y); + debug(LOG_NEVER, "Destination tile blocked (%d;%d)", tileDest.x, tileDest.y); } + const PathNonblockingArea dstIgnore(psJob->dstStructure); NearestSearchPredicate pred(tileOrig); @@ -483,7 +522,7 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, } else { - CostLayer costLayer(pfContext); + IgnoreGoalCostLayer costLayer(pfContext, pred.goal); // Need to find the path from orig to dest, continue previous exploration. fpathAStarReestimate(pfContext, pred.goal); pred.clear(); @@ -511,7 +550,7 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, pfContext.assign(psJob->blockingMap, tileDest, dstIgnore, true); pred.clear(); - CostLayer costLayer(pfContext); + IgnoreGoalCostLayer costLayer(pfContext, pred.goal); // Add the start point to the open list bool started = fpathNewNode(pfContext, pred, costLayer, tileDest, 0, tileDest); ASSERT(started, "fpathNewNode failed to add node."); From bfe17133c05a63ab405b0e944829d2f09a220311 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sat, 29 Jul 2023 18:42:41 +0300 Subject: [PATCH 09/17] Added more profiling sections all over the code --- src/display.cpp | 2 ++ src/map.cpp | 3 +++ src/multiplay.cpp | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/display.cpp b/src/display.cpp index 8ff70056639..ceddbeb0cd0 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -79,6 +79,7 @@ #include "input/manager.h" #include "input/keyconfig.h" #include "mapgrid.h" +#include "profiling.h" InputManager gInputManager; KeyFunctionConfiguration gKeyFuncConfig; @@ -1194,6 +1195,7 @@ void displayWorld() { return; } + WZ_PROFILE_SCOPE(displayWorld); Vector3i pos; diff --git a/src/map.cpp b/src/map.cpp index 71a4634af0e..7ba6001640f 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -52,6 +52,7 @@ #include "fpath.h" #include "levels.h" #include "lib/framework/wzapp.h" +#include "profiling.h" #define GAME_TICKS_FOR_DANGER (GAME_TICKS_PER_SEC * 2) @@ -932,6 +933,7 @@ void WzMapDebugLogger::printLog(WzMap::LoggingProtocol::LogLevel level, const ch /* Initialise the map structure */ bool mapLoad(char const *filename) { + WZ_PROFILE_SCOPE(mapLoad); WzMapPhysFSIO mapIO; WzMapDebugLogger debugLoggerInstance; @@ -1823,6 +1825,7 @@ static void mapFloodFill(int x, int y, int continent, uint8_t blockedBits, uint1 void mapFloodFillContinents() { + WZ_PROFILE_SCOPE(mapFloodFillContinents); int x, y, limitedContinents = 0, hoverContinents = 0; /* Clear continents */ diff --git a/src/multiplay.cpp b/src/multiplay.cpp index 7172ffe6201..f5f892e6b09 100644 --- a/src/multiplay.cpp +++ b/src/multiplay.cpp @@ -85,6 +85,7 @@ #include "spectatorwidgets.h" #include "challenge.h" #include "multilobbycommands.h" +#include "profiling.h" // //////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////// @@ -298,6 +299,7 @@ bool multiplayerWinSequence(bool firstCall) // MultiPlayer main game loop code. bool multiPlayerLoop() { + WZ_PROFILE_SCOPE(multiPlayerLoop); UDWORD i; UBYTE joinCount; From 0d33c1f46861bb253dbceaab5068e9249ce0e98a Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sat, 29 Jul 2023 21:36:12 +0300 Subject: [PATCH 10/17] Added polyline renderer --- lib/ivis_opengl/pieblitfunc.cpp | 20 ++++++++++++++++++++ lib/ivis_opengl/pieblitfunc.h | 2 ++ 2 files changed, 22 insertions(+) diff --git a/lib/ivis_opengl/pieblitfunc.cpp b/lib/ivis_opengl/pieblitfunc.cpp index 428a6269386..6b1a2ca9847 100644 --- a/lib/ivis_opengl/pieblitfunc.cpp +++ b/lib/ivis_opengl/pieblitfunc.cpp @@ -198,6 +198,26 @@ void iV_Lines(const std::vector &lines, PIELIGHT colour) gfx_api::LinePSO::get().unbind_vertex_buffers(pie_internal::rectBuffer); } +void iV_PolyLine(const std::vector &points, const glm::mat4 &mvp, PIELIGHT colour) +{ + std::vector lines; + Vector2i result; + Vector2i lastPoint(0, 0); + + for(auto i = 0; i < points.size(); i++){ + Vector3i source = points[i]; + pie_RotateProjectWithPerspective(&source, mvp, &result); + + if(i > 0){ + lines.push_back({ lastPoint.x, lastPoint.y, result.x, result.y }); + } + + lastPoint = result; + } + + iV_Lines(lines, colour); +} + /** * Assumes render mode set up externally, draws filled rectangle. */ diff --git a/lib/ivis_opengl/pieblitfunc.h b/lib/ivis_opengl/pieblitfunc.h index 18328cd5c49..2ca333d37ae 100644 --- a/lib/ivis_opengl/pieblitfunc.h +++ b/lib/ivis_opengl/pieblitfunc.h @@ -137,6 +137,8 @@ glm::mat4 defaultProjectionMatrix(); void iV_ShadowBox(int x0, int y0, int x1, int y1, int pad, PIELIGHT first, PIELIGHT second, PIELIGHT fill); void iV_Line(int x0, int y0, int x1, int y1, PIELIGHT colour); void iV_Lines(const std::vector &lines, PIELIGHT colour); +/// Draws connected polyline. +void iV_PolyLine(const std::vector &points, const glm::mat4 &mvp, PIELIGHT colour); void iV_Box2(int x0, int y0, int x1, int y1, PIELIGHT first, PIELIGHT second); static inline void iV_Box(int x0, int y0, int x1, int y1, PIELIGHT first) { From 9b49a255a575a6741e96a9bdd40e638f8e952658 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sat, 29 Jul 2023 21:37:33 +0300 Subject: [PATCH 11/17] Added debug renderer for pathfinding system Implemented debug rendering for pathginding: - renderer for blocking layer - rendering of current paths - drawing of path context --- src/display3d.cpp | 1 + src/drawPath.cpp | 225 ++++++++++++++++++++++++++++++++++++++++++++++ src/drawPath.h | 32 +++++++ src/fpath.cpp | 2 +- 4 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 src/drawPath.cpp create mode 100644 src/drawPath.h diff --git a/src/display3d.cpp b/src/display3d.cpp index 93494f88c10..62adf9752a5 100644 --- a/src/display3d.cpp +++ b/src/display3d.cpp @@ -86,6 +86,7 @@ #include "animation.h" #include "faction.h" #include "wzcrashhandlingproviders.h" +#include "drawPath.h" /******************** Prototypes ********************/ diff --git a/src/drawPath.cpp b/src/drawPath.cpp new file mode 100644 index 00000000000..964c4167004 --- /dev/null +++ b/src/drawPath.cpp @@ -0,0 +1,225 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2005-2023 Warzone 2100 Project + + Warzone 2100 is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Warzone 2100 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +/** @file + * Routines for pathfinding-related debug rendering. + */ + + +#include "lib/ivis_opengl/pieblitfunc.h" +#include "lib/ivis_opengl/piematrix.h" +#include "lib/ivis_opengl/textdraw.h" + +#include "droid.h" +#include "objmem.h" +#include "drawPath.h" +#include "display3d.h" + +#include "map.h" + +#include "astar.h" + +// alpha fully opaque +static constexpr PIELIGHT WZ_WHITE {0xFF, 0xFF, 0xFF, 0xFF}; + +void debugRenderText (const char *txt, int vert_idx) +{ + const int TEXT_SPACEMENT = 20; + WzText t(WzString (txt), font_regular); + t.render(20, 80 + TEXT_SPACEMENT * vert_idx, WZ_WHITE); +} + +static void drawLines(const glm::mat4& mvp, std::vector pts, PIELIGHT color) +{ + std::vector grid2D; + for (int i = 0; i < pts.size(); i += 2) + { + Vector2i _a, _b; + pie_RotateProjectWithPerspective(&pts[i], mvp, &_a); + pie_RotateProjectWithPerspective(&pts[i + 1], mvp, &_b); + grid2D.push_back({_a.x, _a.y, _b.x, _b.y}); + } + iV_Lines(grid2D, color); +} + +// draw a square where half of sidelen goes in each direction +void drawSquare (const glm::mat4 &mvp, int sidelen, int startX, int startY, int height, PIELIGHT color) +{ + iV_PolyLine({ + { startX - (sidelen / 2), height, -startY - (sidelen / 2) }, + { startX - (sidelen / 2), height, -startY + (sidelen / 2) }, + { startX + (sidelen / 2), height, -startY + (sidelen / 2) }, + { startX + (sidelen / 2), height, -startY - (sidelen / 2) }, + { startX - (sidelen / 2), height, -startY - (sidelen / 2) }, + }, mvp, color); +} + +// no half-side translation +// world coordinates +static void drawSquare2 (const glm::mat4 &mvp, int sidelen, int startX, int startY, int height, PIELIGHT color) +{ + iV_PolyLine({ + { startX, height, -startY }, + { startX, height, -startY - (sidelen) }, + { startX + (sidelen), height, -startY - (sidelen) }, + { startX + (sidelen), height, -startY }, + { startX, height, -startY}, + }, mvp, color); +} + +void debugDrawImpassableTiles(const PathBlockingMap& bmap, const iView& viewPos, const glm::mat4 &mvp, int tiles = 6) +{ + PathCoord pc = bmap.worldToMap(viewPos.p.x, viewPos.p.z); + const auto playerXTile = pc.x; + const auto playerYTile = pc.y; + const int tileSize = bmap.tileSize(); + const int slice = tileSize / 8; + const int terrainOffset = 10; + + std::vector pts; + + for (int dx = -tiles; dx <= tiles; dx++) + { + for (int dy = -tiles; dy <=tiles; dy++) + { + const auto mapx = playerXTile + dx; + const auto mapy = playerYTile + dy; + auto w = bmap.mapToWorld(mapx, mapy); + auto height = map_TileHeight(mapx, mapy) + terrainOffset; + + if (bmap.isBlocked(mapx, mapy)) + { + drawSquare2 (mvp, 128, w.x, w.y, height, WZCOL_RED); + } + + if (bmap.isDangerous(mapx, mapy)) + { + // 45 degrees lines + for (int p = slice/2; p < tileSize; p += slice) + { + // Bottom left corner + pts.push_back({w.x, height, -(w.y + p)}); + pts.push_back({w.x + p, height, -w.y}); + + // Top right corner + pts.push_back({w.x + tileSize, height, -(w.y + p)}); + pts.push_back({w.x + p, height, -w.y - tileSize}); + } + } + } + } + + if (!pts.empty()) + drawLines (mvp, pts, WZCOL_YELLOW); +} + +void drawDroidPath(DROID *psDroid, const glm::mat4& viewMatrix, const glm::mat4 &perspectiveViewMatrix) +{ + auto status = psDroid->sMove.Status; + if (status == MOVEINACTIVE || status == MOVEPAUSE || status == MOVEWAITROUTE || psDroid->sMove.asPath.empty()) + return; + + const int terrainOffset = 5; + + std::vector pastPoints, futurePoints; + + Vector3i prev; + int currentPt = psDroid->sMove.pathIndex; + for (int i = 0; i < psDroid->sMove.asPath.size(); i++) { + Vector2i pt2 = psDroid->sMove.asPath[i]; + auto height = map_TileHeight(map_coord(pt2.x), map_coord(pt2.y)) + terrainOffset; + Vector3i pt3(pt2.x, height, -pt2.y); + + if (i > 0) { + if (i < currentPt) { + pastPoints.push_back(prev); + pastPoints.push_back(pt3); + } else if (i >= currentPt) { + futurePoints.push_back(prev); + futurePoints.push_back(pt3); + } + } + prev = pt3; + } + + if (futurePoints.size() > 1) + drawLines (perspectiveViewMatrix, futurePoints, WZCOL_GREEN); + if (pastPoints.size() > 1) + drawLines (perspectiveViewMatrix, pastPoints, WZCOL_YELLOW); + + auto height = map_TileHeight(map_coord(psDroid->sMove.target.x), map_coord(psDroid->sMove.target.y)) + terrainOffset; + drawSquare(perspectiveViewMatrix, 32, psDroid->sMove.target.x, psDroid->sMove.target.y, height, WZCOL_GREEN); +} + +/// Draws path tree from the context. +void drawContext(const PathfindContext& context, const glm::mat4 &mvp) +{ + const int terrainOffset = 5; + const int contextIteration = context.iteration; + std::vector pts; + for (int y = 0; y < context.height; y++) + { + for (int x = 0; x < context.width; x++) + { + const PathExploredTile& tile = context.tile(PathCoord(x, y)); + if (tile.iteration != contextIteration) + continue; + auto heightStart = map_TileHeight(x, y) + terrainOffset; + Vector3i start(world_coord(x), heightStart, -world_coord(y)); + pts.push_back(start); + auto heightEnd = map_TileHeight(x - tile.dx/64, y - tile.dy/64) + terrainOffset; + Vector3i end(world_coord(x) - tile.dx , heightEnd, -world_coord(y) + tile.dy); + pts.push_back(end); + } + } + if (!pts.empty()) + drawLines(mvp, pts, WZCOL_LGREEN); +} + +static bool _drawImpassableTiles = true; +static bool _drawContextTree = false; + +extern std::list fpathContexts; + +void drawPathCostLayer(int player, const iView& playerViewPos, const glm::mat4& viewMatrix, const glm::mat4 &perspectiveViewMatrix) +{ + if (player >= MAX_PLAYERS) + return; + + if (_drawImpassableTiles) { + PathBlockingMap bmap; + PathBlockingType pbtype; + pbtype.owner = player; + pbtype.propulsion = PROPULSION_TYPE_WHEELED; + fillBlockingMap(bmap, pbtype); + debugDrawImpassableTiles(bmap, playerViewPos, perspectiveViewMatrix, 6); + } + + for (DROID *psDroid = apsDroidLists[player]; psDroid; psDroid = psDroid->psNext) + { + if (psDroid->selected) + { + drawDroidPath(psDroid, viewMatrix, perspectiveViewMatrix); + } + } + + if (_drawContextTree && !fpathContexts.empty()) + { + drawContext(fpathContexts.back(), perspectiveViewMatrix); + } +} diff --git a/src/drawPath.h b/src/drawPath.h new file mode 100644 index 00000000000..77eb24f9d4a --- /dev/null +++ b/src/drawPath.h @@ -0,0 +1,32 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2005-2024 Warzone 2100 Project + + Warzone 2100 is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Warzone 2100 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +/** @file + * Routines for pathfinding-related debug rendering. + */ + +#ifndef __INCLUDED_SRC_DRAW_PATH_H__ +#define __INCLUDED_SRC_DRAW_PATH_H__ + +#include "lib/framework/vector.h" + +struct iView; + +void drawPathCostLayer(int selectedPlayer, const iView& playerPos, const glm::mat4& viewMatrix, const glm::mat4 &perspectiveViewMatrix); + +#endif // __INCLUDED_SRC_DRAW_PATH_H__ diff --git a/src/fpath.cpp b/src/fpath.cpp index 9c8864c14fe..067a9d183c1 100644 --- a/src/fpath.cpp +++ b/src/fpath.cpp @@ -71,7 +71,7 @@ static uint32_t waitingForResultId; static WZ_SEMAPHORE *waitingForResultSemaphore = nullptr; /// Last recently used list of contexts. -static std::list fpathContexts; +std::list fpathContexts; static PATHRESULT fpathExecute(PATHJOB psJob); From b915f6c9acbda3d56b3433005923123efae2a88f Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sun, 6 Aug 2023 21:39:18 +0300 Subject: [PATCH 12/17] Added toggles to draw debug info for path system --- src/cheat.cpp | 2 ++ src/display3d.cpp | 41 ----------------------------------------- src/display3d.h | 1 - src/drawPath.cpp | 26 ++++++++++++++++++++++---- src/keybind.cpp | 7 ------- src/keybind.h | 9 ++++++++- 6 files changed, 32 insertions(+), 54 deletions(-) diff --git a/src/cheat.cpp b/src/cheat.cpp index fd0b83edff4..10111d059fb 100644 --- a/src/cheat.cpp +++ b/src/cheat.cpp @@ -91,6 +91,8 @@ static CHEAT_ENTRY cheatCodes[] = {"autogame off", kf_AutoGame}, {"shakey", kf_ToggleShakeStatus}, //shakey {"list droids", kf_ListDroids}, + {"showpath", kf_ToggleShowPath}, + {"showpathstat", kf_ShowPathStat}, }; diff --git a/src/display3d.cpp b/src/display3d.cpp index 62adf9752a5..318ac882b51 100644 --- a/src/display3d.cpp +++ b/src/display3d.cpp @@ -160,7 +160,6 @@ static SDWORD rangeCenterX, rangeCenterY, rangeRadius; static bool bDrawProximitys = true; bool godMode; bool showGateways = false; -bool showPath = false; // Skybox data static float wind = 0.0f; @@ -768,41 +767,6 @@ static PIELIGHT structureBrightness(STRUCTURE *psStructure) return buildingBrightness; } - -/// Show all droid movement parts by displaying an explosion at every step -static void showDroidPaths() -{ - if ((graphicsTime / 250 % 2) != 0) - { - return; - } - - if (selectedPlayer >= MAX_PLAYERS) - { - return; // no-op for now - } - - for (DROID *psDroid = apsDroidLists[selectedPlayer]; psDroid; psDroid = psDroid->psNext) - { - if (psDroid->selected && psDroid->sMove.Status != MOVEINACTIVE) - { - const int len = psDroid->sMove.asPath.size(); - for (int i = std::max(psDroid->sMove.pathIndex - 1, 0); i < len; i++) - { - Vector3i pos; - - ASSERT(worldOnMap(psDroid->sMove.asPath[i].x, psDroid->sMove.asPath[i].y), "Path off map!"); - pos.x = psDroid->sMove.asPath[i].x; - pos.z = psDroid->sMove.asPath[i].y; - pos.y = map_Height(pos.x, pos.z) + 16; - - effectGiveAuxVar(80); - addEffect(&pos, EFFECT_EXPLOSION, EXPLOSION_TYPE_LASER, false, nullptr, 0); - } - } - } -} - /// Displays an image for the Network Issue button static void NetworkDisplayImage(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset) { @@ -1180,11 +1144,6 @@ void draw3DScene() drawRangeAtPos(rangeCenterX, rangeCenterY, rangeRadius); } - if (showPath) - { - showDroidPaths(); - } - wzPerfEnd(PERF_MISC); } diff --git a/src/display3d.h b/src/display3d.h index a90c88cd2b2..deaa4b8294a 100644 --- a/src/display3d.h +++ b/src/display3d.h @@ -120,7 +120,6 @@ extern Vector2i mousePos; extern bool bRender3DOnly; extern bool showGateways; -extern bool showPath; extern const Vector2i visibleTiles; /*returns the graphic ID for a droid rank*/ diff --git a/src/drawPath.cpp b/src/drawPath.cpp index 964c4167004..84ccf7f6882 100644 --- a/src/drawPath.cpp +++ b/src/drawPath.cpp @@ -191,8 +191,9 @@ void drawContext(const PathfindContext& context, const glm::mat4 &mvp) drawLines(mvp, pts, WZCOL_LGREEN); } -static bool _drawImpassableTiles = true; +static bool _drawImpassableTiles = false; static bool _drawContextTree = false; +static bool _drawDroidPath = false; extern std::list fpathContexts; @@ -210,11 +211,14 @@ void drawPathCostLayer(int player, const iView& playerViewPos, const glm::mat4& debugDrawImpassableTiles(bmap, playerViewPos, perspectiveViewMatrix, 6); } - for (DROID *psDroid = apsDroidLists[player]; psDroid; psDroid = psDroid->psNext) + if (_drawDroidPath) { - if (psDroid->selected) + for (DROID *psDroid = apsDroidLists[player]; psDroid; psDroid = psDroid->psNext) { - drawDroidPath(psDroid, viewMatrix, perspectiveViewMatrix); + if (psDroid->selected) + { + drawDroidPath(psDroid, viewMatrix, perspectiveViewMatrix); + } } } @@ -223,3 +227,17 @@ void drawPathCostLayer(int player, const iView& playerViewPos, const glm::mat4& drawContext(fpathContexts.back(), perspectiveViewMatrix); } } + +void kf_ToggleShowPath() +{ + addConsoleMessage(_("Path display toggled."), DEFAULT_JUSTIFY, SYSTEM_MESSAGE); + _drawDroidPath = !_drawDroidPath; +} + +void kf_ShowPathStat() { + _drawContextTree = !_drawContextTree; +} + +void kf_ShowPathBlocking() { + _drawImpassableTiles = !_drawImpassableTiles; +} diff --git a/src/keybind.cpp b/src/keybind.cpp index e261b900612..0e85ccb9e25 100644 --- a/src/keybind.cpp +++ b/src/keybind.cpp @@ -161,12 +161,6 @@ void kf_ToggleShowGateways() showGateways = !showGateways; } -void kf_ToggleShowPath() -{ - addConsoleMessage(_("Path display toggled."), DEFAULT_JUSTIFY, SYSTEM_MESSAGE); - showPath = !showPath; -} - void kf_PerformanceSample() { wzPerfStart(); @@ -752,7 +746,6 @@ void kf_ListDroids() } - // -------------------------------------------------------------------------- /* Toggles radar on off */ diff --git a/src/keybind.h b/src/keybind.h index c88c9ada35e..7e13a957dd6 100644 --- a/src/keybind.h +++ b/src/keybind.h @@ -149,7 +149,6 @@ void kf_SetHardLevel(); MappableFunction kf_SelectCommander_N(const unsigned int n); void kf_ToggleShowGateways(); -void kf_ToggleShowPath(); void kf_FaceNorth(); void kf_FaceSouth(); @@ -199,6 +198,14 @@ void kf_ToggleFullscreen(); void kf_ToggleSpecOverlays(); +/// These are defined at drawPath. +/// Toggle debug rendering of droid paths. +void kf_ToggleShowPath(); +/// Toggle debug rendering of a path context. +void kf_ShowPathStat(); + +void kf_ShowPathGrid(); + void enableGodMode(); void keybindShutdown(); From 8549f29f8a4fb04097db75064facb3f7beb24e90 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Mon, 7 Aug 2023 21:32:25 +0300 Subject: [PATCH 13/17] Restored threaded pathfinding --- src/drawPath.cpp | 23 ++++++++++++++--------- src/fpath.cpp | 13 +++++++++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/drawPath.cpp b/src/drawPath.cpp index 84ccf7f6882..6f513a9edf4 100644 --- a/src/drawPath.cpp +++ b/src/drawPath.cpp @@ -34,6 +34,8 @@ #include "astar.h" +bool fpathGetAsyncMode(); + // alpha fully opaque static constexpr PIELIGHT WZ_WHITE {0xFF, 0xFF, 0xFF, 0xFF}; @@ -211,20 +213,23 @@ void drawPathCostLayer(int player, const iView& playerViewPos, const glm::mat4& debugDrawImpassableTiles(bmap, playerViewPos, perspectiveViewMatrix, 6); } - if (_drawDroidPath) - { - for (DROID *psDroid = apsDroidLists[player]; psDroid; psDroid = psDroid->psNext) + // Accessing path contexts or droid's path is not thread safe now. + if (!fpathGetAsyncMode()) { + if (_drawDroidPath) { - if (psDroid->selected) + for (DROID *psDroid = apsDroidLists[player]; psDroid; psDroid = psDroid->psNext) { - drawDroidPath(psDroid, viewMatrix, perspectiveViewMatrix); + if (psDroid->selected) + { + drawDroidPath(psDroid, viewMatrix, perspectiveViewMatrix); + } } } - } - if (_drawContextTree && !fpathContexts.empty()) - { - drawContext(fpathContexts.back(), perspectiveViewMatrix); + if (_drawContextTree && !fpathContexts.empty()) + { + drawContext(fpathContexts.back(), perspectiveViewMatrix); + } } } diff --git a/src/fpath.cpp b/src/fpath.cpp index 067a9d183c1..54517302942 100644 --- a/src/fpath.cpp +++ b/src/fpath.cpp @@ -42,9 +42,14 @@ // If the path finding system is shutdown or not static volatile bool fpathQuit = false; -// Run PF tasks in separate thread. -// Can be switch off to simplify debugging of PF system. -constexpr bool fpathAsyncMode = false; + +/// Check if PF tasks are running in a separate thread. +/// Disabling async mode can simplify debugging of PF system. +/// Making this constant "functional" is more robust and portable than extern const/constexpr. +bool fpathGetAsyncMode() +{ + return true; +} /* Beware: Enabling this will cause significant slow-down. */ #undef DEBUG_MAP @@ -399,7 +404,7 @@ static FPATH_RETVAL fpathRoute(MOVE_CONTROL *psMove, unsigned id, int startX, in // job or result for each droid in the system at any time. fpathRemoveDroidData(id); - if (fpathAsyncMode) { + if (fpathGetAsyncMode()) { wz::packaged_task task([job]() { return fpathExecute(job); }); pathResults[id] = task.get_future(); // Add to end of list From e7b880eaed9753c8e59fffeda8795cf1c6803a33 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Wed, 20 Sep 2023 17:32:27 +0300 Subject: [PATCH 14/17] Refactored generation of PF continents --- src/astar.cpp | 17 ++++- src/display.cpp | 5 +- src/fpath.cpp | 36 +++++++++-- src/fpath.h | 6 ++ src/map.cpp | 78 +---------------------- src/map.h | 6 +- src/path_continents.cpp | 133 ++++++++++++++++++++++++++++++++++++++++ src/path_continents.h | 74 ++++++++++++++++++++++ src/wzapi.cpp | 4 +- 9 files changed, 272 insertions(+), 87 deletions(-) create mode 100644 src/path_continents.cpp create mode 100644 src/path_continents.h diff --git a/src/astar.cpp b/src/astar.cpp index 8c857c9320a..6ecb9264ddf 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -484,6 +484,9 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, const PathCoord tileOrig = psJob->blockingMap->worldToMap(psJob->origX, psJob->origY); const PathCoord tileDest = psJob->blockingMap->worldToMap(psJob->destX, psJob->destY); + int origContinent = fpathGetLandContinent(tileOrig.x, tileOrig.y); + int destContinent = fpathGetLandContinent(tileDest.x, tileDest.y); + if (psJob->blockingMap->isBlocked(tileOrig.x, tileOrig.y)) { debug(LOG_NEVER, "Initial tile blocked (%d;%d)", tileOrig.x, tileOrig.y); } @@ -531,6 +534,13 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, endCoord = pred.nearestCoord; // Found the path! Don't search more contexts. break; + } else { + if (origContinent == destContinent) { + debug(LOG_NEVER, "Failed to find cached path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); + } else { + debug(LOG_NEVER, "Failed to find cached intercontinental path (%d;%d c%d)-(%d;%d c%d)", tileOrig.x, tileOrig.y, origContinent, + tileDest.x, tileDest.y, destContinent); + } } } } @@ -557,7 +567,12 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, ExplorationReport report = fpathAStarExplore(pfContext, pred, costLayer); if (!report) { - debug(LOG_NEVER, "Failed to find path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); + if (origContinent == destContinent) { + debug(LOG_NEVER, "Failed to find path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); + } else { + debug(LOG_NEVER, "Failed to find intercontinental path (%d;%d c%d)-(%d;%d c%d)", tileOrig.x, tileOrig.y, origContinent, + tileDest.x, tileDest.y, destContinent); + } } endCoord = pred.nearestCoord; } diff --git a/src/display.cpp b/src/display.cpp index ceddbeb0cd0..4330f2ac378 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -2076,10 +2076,13 @@ void dealWithLMB() MAPTILE *psTile = mapTile(mouseTileX, mouseTileY); uint8_t aux = auxTile(mouseTileX, mouseTileY, selectedPlayer); + int landContinent = fpathGetLandContinent(mouseTileX, mouseTileY); + int hoverContinent = fpathGetHoverContinent(mouseTileX, mouseTileY); + console("%s tile %d, %d [%d, %d] continent(l%d, h%d) level %g illum %d %s %s w=%d s=%d j=%d", tileIsExplored(psTile) ? "Explored" : "Unexplored", mouseTileX, mouseTileY, world_coord(mouseTileX), world_coord(mouseTileY), - (int)psTile->limitedContinent, (int)psTile->hoverContinent, psTile->level, (int)psTile->illumination, + landContinent, hoverContinent, psTile->level, (int)psTile->illumination, aux & AUXBITS_DANGER ? "danger" : "", aux & AUXBITS_THREAT ? "threat" : "", (int)psTile->watchers[selectedPlayer], (int)psTile->sensors[selectedPlayer], (int)psTile->jammers[selectedPlayer]); } diff --git a/src/fpath.cpp b/src/fpath.cpp index 54517302942..91e8ef7684e 100644 --- a/src/fpath.cpp +++ b/src/fpath.cpp @@ -39,6 +39,8 @@ #include "astar.h" #include "fpath.h" +#include "path_continents.h" +#include "profiling.h" // If the path finding system is shutdown or not static volatile bool fpathQuit = false; @@ -81,6 +83,7 @@ std::list fpathContexts; static PATHRESULT fpathExecute(PATHJOB psJob); static PathMapCache pfMapCache; +static PathContinents pathContinents; /** This runs in a separate thread */ static int fpathThreadFunc(void *) @@ -433,7 +436,7 @@ static FPATH_RETVAL fpathRoute(MOVE_CONTROL *psMove, unsigned id, int startX, in // Find a route for an DROID to a location in world coordinates FPATH_RETVAL fpathDroidRoute(DROID *psDroid, SDWORD tX, SDWORD tY, FPATH_MOVETYPE moveType) { - bool acceptNearest; + bool acceptNearest = true; PROPULSION_STATS *psPropStats = getPropulsionStats(psDroid); // override for AI to blast our way through stuff @@ -645,12 +648,20 @@ bool fpathCheck(Position orig, Position dest, PROPULSION_TYPE propulsion) return false; } - MAPTILE *origTile = worldTile(findNonblockingPosition(orig, propulsion).xy()); - MAPTILE *destTile = worldTile(findNonblockingPosition(dest, propulsion).xy()); + Position nonblockOrig = findNonblockingPosition(orig, propulsion); + Position nonblockDest = findNonblockingPosition(dest, propulsion); + MAPTILE *origTile = worldTile(nonblockOrig.xy()); + MAPTILE *destTile = worldTile(nonblockDest.xy()); ASSERT_OR_RETURN(false, propulsion != PROPULSION_TYPE_NUM, "Bad propulsion type"); ASSERT_OR_RETURN(false, origTile != nullptr && destTile != nullptr, "Bad tile parameter"); + auto origLand = pathContinents.getLand(nonblockOrig); + auto destLand = pathContinents.getLand(nonblockDest); + + auto origHover = pathContinents.getLand(nonblockOrig); + auto destHover = pathContinents.getLand(nonblockDest); + switch (propulsion) { case PROPULSION_TYPE_PROPELLOR: @@ -658,9 +669,9 @@ bool fpathCheck(Position orig, Position dest, PROPULSION_TYPE propulsion) case PROPULSION_TYPE_TRACKED: case PROPULSION_TYPE_LEGGED: case PROPULSION_TYPE_HALF_TRACKED: - return origTile->limitedContinent == destTile->limitedContinent; + return origLand == destLand; case PROPULSION_TYPE_HOVER: - return origTile->hoverContinent == destTile->hoverContinent; + return origHover == destHover; case PROPULSION_TYPE_LIFT: return true; // assume no map uses skyscrapers to isolate areas default: @@ -670,3 +681,18 @@ bool fpathCheck(Position orig, Position dest, PROPULSION_TYPE propulsion) ASSERT(false, "Should never get here, unknown propulsion !"); return false; // should never get here } + +/** Get land continent ID of specified tile coordinate. */ +uint16_t fpathGetLandContinent(int x, int y) { + return pathContinents.getLand(x, y); +} + +/** Get hover continent ID of specified tile coordinate. */ +uint16_t fpathGetHoverContinent(int x, int y) { + return pathContinents.getHover(x, y); +} + +void mapFloodFillContinents() +{ + pathContinents.generate(); +} diff --git a/src/fpath.h b/src/fpath.h index c6ff44ee4ba..aa5928b47c7 100644 --- a/src/fpath.h +++ b/src/fpath.h @@ -124,6 +124,12 @@ bool fpathCheck(Position orig, Position dest, PROPULSION_TYPE propulsion); /** Unit testing. */ void fpathTest(int x, int y, int x2, int y2); +/** Get land continent ID of specified tile coordinate. */ +uint16_t fpathGetLandContinent(int x, int y); + +/** Get hover continent ID of specified tile coordinate. */ +uint16_t fpathGetHoverContinent(int x, int y); + /** @} */ #endif // __INCLUDED_SRC_FPATH_H__ diff --git a/src/map.cpp b/src/map.cpp index 7ba6001640f..cd105bd36a2 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -1788,82 +1788,8 @@ static const Vector2i aDirOffset[] = Vector2i(1, 1), }; -// Flood fill a "continent". -// TODO take into account scroll limits and update continents on scroll limit changes -static void mapFloodFill(int x, int y, int continent, uint8_t blockedBits, uint16_t MAPTILE::*varContinent) -{ - std::vector open; - open.push_back(Vector2i(x, y)); - mapTile(x, y)->*varContinent = continent; // Set continent value - - while (!open.empty()) - { - // Pop the first open node off the list for this iteration - Vector2i pos = open.back(); - open.pop_back(); - - // Add accessible neighbouring tiles to the open list - for (int i = 0; i < NUM_DIR; ++i) - { - // rely on the fact that all border tiles are inaccessible to avoid checking explicitly - Vector2i npos = pos + aDirOffset[i]; - - if (npos.x < 1 || npos.y < 1 || npos.x > mapWidth - 2 || npos.y > mapHeight - 2) - { - continue; - } - MAPTILE *psTile = mapTile(npos); - - if (!(blockTile(npos.x, npos.y, AUX_MAP) & blockedBits) && psTile->*varContinent == 0) - { - open.push_back(npos); // add to open list - psTile->*varContinent = continent; // Set continent value - } - } - } -} - -void mapFloodFillContinents() -{ - WZ_PROFILE_SCOPE(mapFloodFillContinents); - int x, y, limitedContinents = 0, hoverContinents = 0; - - /* Clear continents */ - for (y = 0; y < mapHeight; y++) - { - for (x = 0; x < mapWidth; x++) - { - MAPTILE *psTile = mapTile(x, y); - - psTile->limitedContinent = 0; - psTile->hoverContinent = 0; - } - } - - /* Iterate over the whole map, looking for unset continents */ - for (y = 1; y < mapHeight - 2; y++) - { - for (x = 1; x < mapWidth - 2; x++) - { - MAPTILE *psTile = mapTile(x, y); - - if (psTile->limitedContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_WHEELED)) - { - mapFloodFill(x, y, 1 + limitedContinents++, WATER_BLOCKED | FEATURE_BLOCKED, &MAPTILE::limitedContinent); - } - else if (psTile->limitedContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_PROPELLOR)) - { - mapFloodFill(x, y, 1 + limitedContinents++, LAND_BLOCKED | FEATURE_BLOCKED, &MAPTILE::limitedContinent); - } - - if (psTile->hoverContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_HOVER)) - { - mapFloodFill(x, y, 1 + hoverContinents++, FEATURE_BLOCKED, &MAPTILE::hoverContinent); - } - } - } - debug(LOG_MAP, "Found %d limited and %d hover continents", limitedContinents, hoverContinents); -} +// Defined at fpath.cpp +void mapFloodFillContinents(); void tileSetFire(int32_t x, int32_t y, uint32_t duration) { diff --git a/src/map.h b/src/map.h index ad866e72652..09ae2ccaf8e 100644 --- a/src/map.h +++ b/src/map.h @@ -62,8 +62,6 @@ struct MAPTILE float level; ///< The visibility level of the top left of the tile, for this client. BASE_OBJECT * psObject; // Any object sitting on the location (e.g. building) PIELIGHT colour; - uint16_t limitedContinent; ///< For land or sea limited propulsion types - uint16_t hoverContinent; ///< For hover type propulsions uint8_t ground; ///< The ground type used for the terrain renderer uint16_t fireEndTime; ///< The (uint16_t)(gameTime / GAME_TICKS_PER_UPDATE) that BITS_ON_FIRE should be cleared. int32_t waterLevel; ///< At what height is the water for this tile @@ -566,6 +564,10 @@ WZ_DECL_ALWAYS_INLINE static inline bool hasSensorOnTile(MAPTILE *psTile, unsign } void mapInit(); + +/** + * Updates danger and burning map. + */ void mapUpdate(); bool loadTerrainTypeMapOverride(MAP_TILESET tileSet); diff --git a/src/path_continents.cpp b/src/path_continents.cpp new file mode 100644 index 00000000000..2a01b462381 --- /dev/null +++ b/src/path_continents.cpp @@ -0,0 +1,133 @@ +#include "map.h" +#include "path_continents.h" +#include "fpath.h" // for fpathBlockingTile +#include "profiling.h" + +// Convert a direction into an offset. +// dir 0 => x = 0, y = -1 +static const std::array aDirOffset = +{ + Vector2i(0, 1), + Vector2i(-1, 1), + Vector2i(-1, 0), + Vector2i(-1, -1), + Vector2i(0, -1), + Vector2i(1, -1), + Vector2i(1, 0), + Vector2i(1, 1), +}; + +// Flood fill a "continent". +// TODO take into account scroll limits and update continents on scroll limit changes +size_t PathContinents::mapFloodFill(int x, int y, ContinentId continent, uint8_t blockedBits, ContinentId* continentGrid) +{ + std::vector open; + open.push_back(Vector2i(x, y)); + // Set continent value for the root of the search. + continentGrid[x + y * width] = continent; + + size_t visitedTiles = 0; + + while (!open.empty()) + { + // Pop the first open node off the list for this iteration + Vector2i pos = open.back(); + open.pop_back(); + + // Add accessible neighboring tiles to the open list + for (int i = 0; i < aDirOffset.size(); ++i) + { + // rely on the fact that all border tiles are inaccessible to avoid checking explicitly + Vector2i npos = pos + aDirOffset[i]; + + if (npos.x < 1 || npos.y < 1 || npos.x > mapWidth - 2 || npos.y > mapHeight - 2) + { + continue; + } + + ContinentId& id = continentGrid[npos.x + npos.y * width]; + + if (!(blockTile(npos.x, npos.y, AUX_MAP) & blockedBits) && id == 0) + { + // add to open list + open.push_back(npos); + // Set continent value + id = continent; + } + } + } + return visitedTiles; +} + +void PathContinents::clear() { + limitedContinents = 0; + hoverContinents = 0; + for (size_t i = 0; i < landGrid.size(); i++) { + landGrid[i] = 0; + } + for (size_t i = 0; i < hoverGrid.size(); i++) { + hoverGrid[i] = 0; + } +} + +void PathContinents::generate() { + WZ_PROFILE_SCOPE2(PathContinents, generate); + /* Clear continents */ + clear(); + + width = mapWidth; + height = mapHeight; + tileShift = TILE_SHIFT; + + landGrid.resize(width*height, ContinentId(0)); + hoverGrid.resize(width*height, ContinentId(0)); + + /* Iterate over the whole map, looking for unset continents */ + for (int y = 1; y < height - 2; y++) + { + for (int x = 1; x < width - 2; x++) + { + if (landGrid[x + y * width] == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_WHEELED)) + { + mapFloodFill(x, y, 1 + limitedContinents++, WATER_BLOCKED | FEATURE_BLOCKED, landGrid.data()); + } + else if (landGrid[x + y * width] == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_PROPELLOR)) + { + mapFloodFill(x, y, 1 + limitedContinents++, LAND_BLOCKED | FEATURE_BLOCKED, landGrid.data()); + } + + if (hoverGrid[x + y * width] && !fpathBlockingTile(x, y, PROPULSION_TYPE_HOVER)) + { + mapFloodFill(x, y, 1 + hoverContinents++, FEATURE_BLOCKED, hoverGrid.data()); + } + } + } + + debug(LOG_MAP, "Found %d limited and %d hover continents", limitedContinents, hoverContinents); +} + +PathContinents::ContinentId PathContinents::getLand(int x, int y) const { + if (x >= 0 && y >= 0 && x < width && y < height) { + return landGrid[x + y * width]; + } + return 0; +} + +PathContinents::ContinentId PathContinents::getHover(int x, int y) const { + if (x >= 0 && y >= 0 && x < width && y < height) { + return hoverGrid[x + y * width]; + } + return 0; +} + +PathContinents::ContinentId PathContinents::getLand(const Position& worldPos) const { + int x = worldPos.x >> tileShift; + int y = worldPos.y >> tileShift; + return getLand(x, y); +} + +PathContinents::ContinentId PathContinents::getHover(const Position& worldPos) const { + int x = worldPos.x >> tileShift; + int y = worldPos.y >> tileShift; + return getHover(x, y); +} diff --git a/src/path_continents.h b/src/path_continents.h new file mode 100644 index 00000000000..7024d3f6d5b --- /dev/null +++ b/src/path_continents.h @@ -0,0 +1,74 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2005-2023 Warzone 2100 Project + + Warzone 2100 is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Warzone 2100 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef __INCLUDED_SRC_PATH_CONTINENTS_H__ +#define __INCLUDED_SRC_PATH_CONTINENTS_H__ + +#include +#include + +/// Container for a "Continents" +/// A continent is an isolated area on the map. +class PathContinents { +public: + struct ContinentInfo { + /// Number of tiles in a continent. + size_t size = 0; + /// Blocking flags of a continent. + uint8_t blockedBits; + /// Starting position of a floodFill, used to build this continent. + int x; + int y; + }; + using ContinentId = uint16_t; + + void generate(); + + void clear(); + + /// Get continent ID from tile coordinate. + ContinentId getLand(int x, int y) const; + + /// Get continent ID from world coordinate. + ContinentId getLand(const Position& worldPos) const; + + /// Get continent ID from tile coordinate. + ContinentId getHover(int x, int y) const; + + /// Get continent ID from world coordinate. + ContinentId getHover(const Position& worldPos) const; + +protected: + size_t mapFloodFill(int x, int y, ContinentId continent, uint8_t blockedBits, ContinentId* continentGrid); + + /// Local geometry of a map. + /// It can differ from dimensions of global map. + int width = 0, height = 0; + int tileShift = 0; + + std::vector continents; + /// 2d layer of continent IDs for each map tile. + std::vector landGrid; + std::vector hoverGrid; + + ContinentId limitedContinents = 0; + ContinentId hoverContinents = 0; +}; + +#endif // __INCLUDED_SRC_PATH_CONTINENTS_H__ diff --git a/src/wzapi.cpp b/src/wzapi.cpp index d2f754e3bf2..307b6175904 100644 --- a/src/wzapi.cpp +++ b/src/wzapi.cpp @@ -4528,8 +4528,8 @@ nlohmann::json wzapi::constructMapTilesArray() nlohmann::json mapTile = nlohmann::json::object(); mapTile["terrainType"] = ::terrainType(psTile); mapTile["height"] = psTile->height; - mapTile["hoverContinent"] = psTile->hoverContinent; - mapTile["limitedContinent"] = psTile->limitedContinent; + mapTile["hoverContinent"] = fpathGetHoverContinent(x, y); + mapTile["limitedContinent"] = fpathGetLandContinent(x, y); mapRow.push_back(std::move(mapTile)); } mapTileArray.push_back(std::move(mapRow)); From d93405f92958d8f99bd0e2acda2fea262afb5923 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sun, 22 Oct 2023 20:48:07 +0300 Subject: [PATCH 15/17] Exposed new command line option to enable PF debug --- src/clparse.cpp | 6 +++++- src/fpath.cpp | 24 +++++++++++++++--------- src/warzoneconfig.h | 8 ++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/clparse.cpp b/src/clparse.cpp index fb8bb21436a..eab4ce37884 100644 --- a/src/clparse.cpp +++ b/src/clparse.cpp @@ -350,6 +350,7 @@ typedef enum CLI_ADD_LOBBY_ADMINPUBLICKEY, CLI_COMMAND_INTERFACE, CLI_STARTPLAYERS, + CLI_DEBUG_PATHFINDING, } CLI_OPTIONS; // Separate table that avoids *any* translated strings, to avoid any risk of gettext / libintl function calls @@ -429,6 +430,7 @@ static const struct poptOption *getOptionsTable() { "addlobbyadminpublickey", POPT_ARG_STRING, CLI_ADD_LOBBY_ADMINPUBLICKEY, N_("Add a lobby admin public key (for slash commands)"), N_("b64-pub-key")}, { "enablecmdinterface", POPT_ARG_STRING, CLI_COMMAND_INTERFACE, N_("Enable command interface"), N_("(stdin)")}, { "startplayers", POPT_ARG_STRING, CLI_STARTPLAYERS, N_("Minimum required players to auto-start game"), N_("startplayers")}, + { "pathdebug", POPT_ARG_NONE, CLI_DEBUG_PATHFINDING, N_("Enable debug mode for pathfinding"), nullptr }, // Terminating entry { nullptr, 0, 0, nullptr, nullptr }, }; @@ -1032,7 +1034,9 @@ bool ParseCommandLine(int argc, const char * const *argv) } debug(LOG_INFO, "Games will automatically start with [%d] players (when ready)", wz_min_autostart_players); break; - + case CLI_DEBUG_PATHFINDING: + war_fpathEnableDebug(); + break; }; } diff --git a/src/fpath.cpp b/src/fpath.cpp index 91e8ef7684e..8dc9fb4981b 100644 --- a/src/fpath.cpp +++ b/src/fpath.cpp @@ -44,14 +44,7 @@ // If the path finding system is shutdown or not static volatile bool fpathQuit = false; - -/// Check if PF tasks are running in a separate thread. -/// Disabling async mode can simplify debugging of PF system. -/// Making this constant "functional" is more robust and portable than extern const/constexpr. -bool fpathGetAsyncMode() -{ - return true; -} +static volatile bool pathAsyncMode = true; /* Beware: Enabling this will cause significant slow-down. */ #undef DEBUG_MAP @@ -85,6 +78,19 @@ static PATHRESULT fpathExecute(PATHJOB psJob); static PathMapCache pfMapCache; static PathContinents pathContinents; +void war_fpathEnableDebug() { + // Disabling async mode only if PF thread has not started. + if (!fpathThread) + pathAsyncMode = false; +} +/// Check if PF tasks are running in a separate thread. +/// Disabling async mode can simplify debugging of PF system. +/// Making this constant "functional" is more robust and portable than extern const/constexpr. +bool fpathGetAsyncMode() +{ + return pathAsyncMode; +} + /** This runs in a separate thread */ static int fpathThreadFunc(void *) { @@ -130,7 +136,7 @@ bool fpathInitialise() // The path system is up fpathQuit = false; - if (!fpathThread) + if (pathAsyncMode && !fpathThread) { fpathMutex = wzMutexCreate(); fpathSemaphore = wzSemaphoreCreate(0); diff --git a/src/warzoneconfig.h b/src/warzoneconfig.h index de1d1256e75..f62819058e9 100644 --- a/src/warzoneconfig.h +++ b/src/warzoneconfig.h @@ -165,4 +165,12 @@ void war_setSoundEnabled(bool soundEnabled); */ bool war_getSoundEnabled(); +/** + * Enables debug mode for pathfinding: + * - enables debug graphics + * - PF requests are processed in the main thread. + * This mode is enabled from command line. + */ +void war_fpathEnableDebug(); + #endif // __INCLUDED_SRC_WARZONECONFIG_H__ From 0d41be2cba9d977422ade3735457b2f761ef4359 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sun, 22 Oct 2023 20:48:54 +0300 Subject: [PATCH 16/17] Updated condition for debug rendering of paths --- src/drawPath.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/drawPath.cpp b/src/drawPath.cpp index 6f513a9edf4..199277a09af 100644 --- a/src/drawPath.cpp +++ b/src/drawPath.cpp @@ -213,19 +213,19 @@ void drawPathCostLayer(int player, const iView& playerViewPos, const glm::mat4& debugDrawImpassableTiles(bmap, playerViewPos, perspectiveViewMatrix, 6); } - // Accessing path contexts or droid's path is not thread safe now. - if (!fpathGetAsyncMode()) { - if (_drawDroidPath) + if (_drawDroidPath) + { + for (DROID *psDroid = apsDroidLists[player]; psDroid; psDroid = psDroid->psNext) { - for (DROID *psDroid = apsDroidLists[player]; psDroid; psDroid = psDroid->psNext) + if (psDroid->selected) { - if (psDroid->selected) - { - drawDroidPath(psDroid, viewMatrix, perspectiveViewMatrix); - } + drawDroidPath(psDroid, viewMatrix, perspectiveViewMatrix); } } + } + // Accessing path contexts is not thread safe now. + if (!fpathGetAsyncMode()) { if (_drawContextTree && !fpathContexts.empty()) { drawContext(fpathContexts.back(), perspectiveViewMatrix); From f7c9d8afd2cb45d09a0bb007d4d3e4b50148cf65 Mon Sep 17 00:00:00 2001 From: Dmitry Kargin Date: Sun, 22 Oct 2023 21:41:56 +0300 Subject: [PATCH 17/17] Added a fallback forward PF search for intercontinental queries --- src/astar.cpp | 212 ++++++++++++++++++++++++++++++-------------------- src/fpath.cpp | 18 +++++ src/fpath.h | 4 + 3 files changed, 150 insertions(+), 84 deletions(-) diff --git a/src/astar.cpp b/src/astar.cpp index 6ecb9264ddf..d494d8e29e2 100644 --- a/src/astar.cpp +++ b/src/astar.cpp @@ -371,7 +371,15 @@ static ExplorationReport fpathAStarExplore(PathfindContext &context, Predicate& constexpr int adjacency = 8; while (!context.nodes.empty()) { - PathNode node = fpathTakeNode(context.nodes); + // Special care for reusable contexts: if we extract the node before checking the goal, + // then the neighbors of this node will not be properly expanded if we reuse this context. + // It can lead to incorrect paths in subsequent searches. + PathNode node = context.nodes.front(); + if (predicate.isGoal(node)) { + report.success = true; + break; + } + fpathTakeNode(context.nodes); report.tilesExplored++; report.cost = node.dist; @@ -380,11 +388,6 @@ static ExplorationReport fpathAStarExplore(PathfindContext &context, Predicate& continue; tile.visited = true; - if (predicate.isGoal(node)) { - report.success = true; - break; - } - /* 5 6 7 \|/ @@ -474,18 +477,25 @@ static ASR_RETVAL fpathTracePath(const PathfindContext& context, PathCoord src, ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, MOVE_CONTROL *psMove, PATHJOB *psJob) { + // Default context for forward searches. + static PathfindContext forwardContext; + ASSERT_OR_RETURN(ASR_FAILED, psMove, "Null psMove"); ASSERT_OR_RETURN(ASR_FAILED, psJob, "Null psMove"); ASR_RETVAL retval = ASR_OK; - bool mustReverse = false; - const PathCoord tileOrig = psJob->blockingMap->worldToMap(psJob->origX, psJob->origY); const PathCoord tileDest = psJob->blockingMap->worldToMap(psJob->destX, psJob->destY); - int origContinent = fpathGetLandContinent(tileOrig.x, tileOrig.y); - int destContinent = fpathGetLandContinent(tileDest.x, tileDest.y); + int origContinent = fpathGetContinent(tileOrig.x, tileOrig.y, psJob->propulsion); + int destContinent = fpathGetContinent(tileDest.x, tileDest.y, psJob->propulsion); + + bool mustReverse = false; + bool intercontinentalSearch = (origContinent != destContinent); + + //if (origContinent != destContinent) + // return ASR_FAILED; if (psJob->blockingMap->isBlocked(tileOrig.x, tileOrig.y)) { debug(LOG_NEVER, "Initial tile blocked (%d;%d)", tileOrig.x, tileOrig.y); @@ -496,76 +506,23 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, const PathNonblockingArea dstIgnore(psJob->dstStructure); - NearestSearchPredicate pred(tileOrig); - PathCoord endCoord; + // Pointer to the context, to be used for finalization of the search. + PathfindContext* pContext = nullptr; - // Caching reverse searches. - std::list::iterator contextIterator = fpathContexts.begin(); - for (; contextIterator != fpathContexts.end(); ++contextIterator) - { - PathfindContext& pfContext = *contextIterator; - if (!pfContext.matches(psJob->blockingMap, tileDest, dstIgnore, /*reverse*/true)) - { - // This context is not for the same droid type and same destination. - continue; - } - - const PathExploredTile& pt = pfContext.tile(tileOrig); - // We have tried going to tileDest before. - if (pfContext.isTileVisited(pt)) - { - // Already know the path from orig to dest. - endCoord = tileOrig; - } - else if (pfContext.nodes.empty()) { - // Wave has already collapsed. Consequent attempt to search will exit immediately. - // We can be here only if there is literally no path existing. - continue; - } - else - { - IgnoreGoalCostLayer costLayer(pfContext, pred.goal); - // Need to find the path from orig to dest, continue previous exploration. - fpathAStarReestimate(pfContext, pred.goal); - pred.clear(); - ExplorationReport report = fpathAStarExplore(pfContext, pred, costLayer); - if (report) { - endCoord = pred.nearestCoord; - // Found the path! Don't search more contexts. - break; - } else { - if (origContinent == destContinent) { - debug(LOG_NEVER, "Failed to find cached path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); - } else { - debug(LOG_NEVER, "Failed to find cached intercontinental path (%d;%d c%d)-(%d;%d c%d)", tileOrig.x, tileOrig.y, origContinent, - tileDest.x, tileDest.y, destContinent); - } - } - } - } - - if (contextIterator == fpathContexts.end()) - { - // We did not find an appropriate context. Make one. - if (fpathContexts.size() < 30) - { - fpathContexts.emplace_back(); - } - contextIterator--; - PathfindContext& pfContext = fpathContexts.back(); + if (intercontinentalSearch) { + NearestSearchPredicate forwardSearchPred(tileDest); // Init a new context, overwriting the oldest one if we are caching too many. // We will be searching from orig to dest, since we don't know where the nearest reachable tile to dest is. - pfContext.assign(psJob->blockingMap, tileDest, dstIgnore, true); - pred.clear(); + forwardContext.assign(psJob->blockingMap, tileDest, dstIgnore, true); - IgnoreGoalCostLayer costLayer(pfContext, pred.goal); + IgnoreGoalCostLayer costLayer(forwardContext, forwardSearchPred.goal); // Add the start point to the open list - bool started = fpathNewNode(pfContext, pred, costLayer, tileDest, 0, tileDest); + bool started = fpathNewNode(forwardContext, forwardSearchPred, costLayer, tileOrig, 0, tileOrig); ASSERT(started, "fpathNewNode failed to add node."); - ExplorationReport report = fpathAStarExplore(pfContext, pred, costLayer); + ExplorationReport report = fpathAStarExplore(forwardContext, forwardSearchPred, costLayer); if (!report) { if (origContinent == destContinent) { debug(LOG_NEVER, "Failed to find path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); @@ -574,20 +531,113 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, tileDest.x, tileDest.y, destContinent); } } - endCoord = pred.nearestCoord; + endCoord = forwardSearchPred.nearestCoord; + // return the nearest route if no actual route was found + if (endCoord != forwardSearchPred.goal) + { + retval = ASR_NEAREST; + } + + pContext = &forwardContext; } + else { + NearestSearchPredicate backSearchPred(tileOrig); + std::list::iterator contextIterator; + // Caching reverse searches. + for (contextIterator = fpathContexts.begin(); contextIterator != fpathContexts.end(); ++contextIterator) + { + PathfindContext& pfContext = *contextIterator; + if (!pfContext.matches(psJob->blockingMap, tileDest, dstIgnore, /*reverse*/true)) + { + // This context is not for the same droid type and same destination. + continue; + } - PathfindContext &context = *contextIterator; + const PathExploredTile& pt = pfContext.tile(tileOrig); + // We have tried going to tileDest before. + if (pfContext.isTileVisited(pt)) + { + // Already know the path from orig to dest. + endCoord = tileOrig; + } + else if (pfContext.nodes.empty()) { + // Wave has already collapsed. Consequent attempt to search will exit immediately. + // We can be here only if there is literally no path existing. + continue; + } + else + { + IgnoreGoalCostLayer costLayer(pfContext, backSearchPred.goal); + // Need to find the path from orig to dest, continue previous exploration. + fpathAStarReestimate(pfContext, backSearchPred.goal); + backSearchPred.clear(); + ExplorationReport report = fpathAStarExplore(pfContext, backSearchPred, costLayer); + if (!report) { + if (origContinent == destContinent) { + debug(LOG_NEVER, "Failed to find cached path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); + } else { + debug(LOG_NEVER, "Failed to find cached intercontinental path (%d;%d c%d)-(%d;%d c%d)", tileOrig.x, tileOrig.y, origContinent, + tileDest.x, tileDest.y, destContinent); + } + } + endCoord = backSearchPred.nearestCoord; + // There should be only one context, compatible for reusing. If this path failed, + // then other contexts will fail as well. So we can safely exit this loop. + break; + } + } - // return the nearest route if no actual route was found - if (endCoord != pred.goal) - { - retval = ASR_NEAREST; + if (contextIterator == fpathContexts.end()) + { + // We did not find an appropriate context. Make one. + if (fpathContexts.size() < 30) + { + fpathContexts.emplace_back(); + } + contextIterator--; + PathfindContext& pfContext = fpathContexts.back(); + + // Init a new context, overwriting the oldest one if we are caching too many. + // We will be searching from orig to dest, since we don't know where the nearest reachable tile to dest is. + pfContext.assign(psJob->blockingMap, tileDest, dstIgnore, true); + backSearchPred.clear(); + + IgnoreGoalCostLayer costLayer(pfContext, backSearchPred.goal); + // Add the start point to the open list + bool started = fpathNewNode(pfContext, backSearchPred, costLayer, tileDest, 0, tileDest); + ASSERT(started, "fpathNewNode failed to add node."); + + ExplorationReport report = fpathAStarExplore(pfContext, backSearchPred, costLayer); + if (!report) { + if (origContinent == destContinent) { + debug(LOG_NEVER, "Failed to find path (%d;%d)-(%d;%d)", tileOrig.x, tileOrig.y, tileDest.x, tileDest.y); + } else { + debug(LOG_NEVER, "Failed to find intercontinental path (%d;%d c%d)-(%d;%d c%d)", tileOrig.x, tileOrig.y, origContinent, + tileDest.x, tileDest.y, destContinent); + } + } + endCoord = backSearchPred.nearestCoord; + } + ASSERT_OR_RETURN(ASR_FAILED, contextIterator != fpathContexts.end(), "Missed PF context iterator"); + pContext = &*contextIterator; + + // return the nearest route if no actual route was found + if (endCoord != backSearchPred.goal) + { + retval = ASR_NEAREST; + } + + // Move context to beginning of last recently used list. + if (contextIterator != fpathContexts.begin()) // Not sure whether or not the splice is a safe noop, if equal. + { + fpathContexts.splice(fpathContexts.begin(), fpathContexts, contextIterator); + } } + static std::vector path; // Declared static to save allocations. - ASR_RETVAL traceRet = fpathTracePath(context, endCoord, tileDest, path); + ASR_RETVAL traceRet = fpathTracePath(*pContext, endCoord, tileDest, path); if (traceRet != ASR_OK) return traceRet; @@ -632,12 +682,6 @@ ASR_RETVAL fpathAStarRoute(std::list& fpathContexts, psMove->destination = psMove->asPath[path.size() - 1]; - // Move context to beginning of last recently used list. - if (contextIterator != fpathContexts.begin()) // Not sure whether or not the splice is a safe noop, if equal. - { - fpathContexts.splice(fpathContexts.begin(), fpathContexts, contextIterator); - } - return retval; } diff --git a/src/fpath.cpp b/src/fpath.cpp index 8dc9fb4981b..54ffe360d2a 100644 --- a/src/fpath.cpp +++ b/src/fpath.cpp @@ -698,6 +698,24 @@ uint16_t fpathGetHoverContinent(int x, int y) { return pathContinents.getHover(x, y); } +uint16_t fpathGetContinent(int x, int y, PROPULSION_TYPE propulsion) { + switch (propulsion) + { + case PROPULSION_TYPE_PROPELLOR: + case PROPULSION_TYPE_WHEELED: + case PROPULSION_TYPE_TRACKED: + case PROPULSION_TYPE_LEGGED: + case PROPULSION_TYPE_HALF_TRACKED: + return pathContinents.getLand(x, y); + case PROPULSION_TYPE_HOVER: + return pathContinents.getHover(x, y); + case PROPULSION_TYPE_LIFT: + default: + return 0; + } + return 0; +} + void mapFloodFillContinents() { pathContinents.generate(); diff --git a/src/fpath.h b/src/fpath.h index aa5928b47c7..9092641569d 100644 --- a/src/fpath.h +++ b/src/fpath.h @@ -130,6 +130,10 @@ uint16_t fpathGetLandContinent(int x, int y); /** Get hover continent ID of specified tile coordinate. */ uint16_t fpathGetHoverContinent(int x, int y); +/** Get continent ID of a tile coordinate and specific propulsion. */ +uint16_t fpathGetContinent(int x, int y, PROPULSION_TYPE propulsion); + + /** @} */ #endif // __INCLUDED_SRC_FPATH_H__