// SONIC ROBO BLAST 2 KART //----------------------------------------------------------------------------- // Copyright (C) 2018-2020 by Sean "Sryder" Ryder // Copyright (C) 2018-2020 by Kart Krew // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file k_pathfind.c /// \brief A* Pathfinding algorithm implementation for SRB2 code base. #include "k_pathfind.h" #include "doomdef.h" #include "z_zone.h" #include "k_bheap.h" static const size_t DEFAULT_NODEARRAY_CAPACITY = 8U; static const size_t DEFAULT_OPENSET_CAPACITY = 8U; static const size_t DEFAULT_CLOSEDSET_CAPACITY = 8U; /*-------------------------------------------------- static UINT32 K_NodeGetFScore(const pathfindnode_t *const node) Gets the FScore of a node. The FScore is the GScore plus the HScore. Input Arguments:- node - The node to get the FScore of Return:- The FScore of the node. --------------------------------------------------*/ static UINT32 K_NodeGetFScore(const pathfindnode_t *const node) { UINT32 fscore = UINT32_MAX; I_Assert(node != NULL); fscore = node->gscore + node->hscore; return fscore; } /*-------------------------------------------------- static void K_NodeUpdateHeapIndex(void *const node, const size_t newheapindex) A callback for the Openset Binary Heap to be able to update the heapindex of the pathfindnodes when they are moved. Input Arguments:- node - The node that has been updated, should be a pointer to a pathfindnode_t newheapindex - The new heapindex of the node. Return:- None --------------------------------------------------*/ static void K_NodeUpdateHeapIndex(void *const node, const size_t newheapindex) { if (node == NULL) { CONS_Debug(DBG_GAMELOGIC, "NULL node in K_NodeUpdateHeapIndex.\n"); } else { pathfindnode_t *truenode = (pathfindnode_t*)node; truenode->heapindex = newheapindex; } } /*-------------------------------------------------- static pathfindnode_t *K_NodesArrayContainsNodeData( pathfindnode_t *nodesarray, void* nodedata, size_t nodesarraycount) Checks whether the Nodes Array contains a node with a waypoint. Searches from the end to the start for speed reasons. Input Arguments:- nodesarray - The nodes array within the A* algorithm waypoint - The waypoint to check is within the nodes array nodesarraycount - The current size of the nodes array Return:- The pathfind node that has the waypoint if there is one. NULL if the waypoint is not in the nodes array. --------------------------------------------------*/ static pathfindnode_t *K_NodesArrayContainsNodeData( pathfindnode_t *nodesarray, void* nodedata, size_t nodesarraycount) { pathfindnode_t *foundnode = NULL; size_t i = 0U; I_Assert(nodesarray != NULL); I_Assert(nodedata != NULL); // It is more likely that we'll find the node we are looking for from the end of the array // Yes, the for loop looks weird, remember that size_t is unsigned and we want to check 0, after it hits 0 it // will loop back up to SIZE_MAX for (i = nodesarraycount - 1U; i < nodesarraycount; i--) { if (nodesarray[i].nodedata == nodedata) { foundnode = &nodesarray[i]; break; } } return foundnode; } /*-------------------------------------------------- static boolean K_ClosedsetContainsNode(pathfindnode_t **closedset, pathfindnode_t *node, size_t closedsetcount) Checks whether the Closedset contains a node. Searches from the end to the start for speed reasons. Input Arguments:- closedset - The closed set within the A* algorithm node - The node to check is within the closed set closedsetcount - The current size of the closedset Return:- True if the node is in the closed set, false if it isn't --------------------------------------------------*/ static boolean K_ClosedsetContainsNode(pathfindnode_t **closedset, pathfindnode_t *node, size_t closedsetcount) { boolean nodeisinclosedset = false; size_t i = 0U; I_Assert(closedset != NULL); I_Assert(node != NULL); // It is more likely that we'll find the node we are looking for from the end of the array // Yes, the for loop looks weird, remember that size_t is unsigned and we want to check 0, after it hits 0 it // will loop back up to SIZE_MAX for (i = closedsetcount - 1U; i < closedsetcount; i--) { if (closedset[i] == node) { nodeisinclosedset = true; break; } } return nodeisinclosedset; } /*-------------------------------------------------- static boolean K_PathfindSetupValid(const pathfindsetup_t *const pathfindsetup) Checks that the setup given for pathfinding is valid and can be used. Input Arguments:- pathfindsetup - The setup for the pathfinding given Return:- True if pathfinding setup is valid, false if it isn't. --------------------------------------------------*/ static boolean K_PathfindSetupValid(const pathfindsetup_t *const pathfindsetup) { boolean pathfindsetupvalid = false; size_t sourcenodenumconnectednodes = 0U; size_t endnodenumconnectednodes = 0U; if (pathfindsetup == NULL) { CONS_Debug(DBG_GAMELOGIC, "NULL pathfindsetup in K_PathfindSetupValid.\n"); } else if (pathfindsetup->startnodedata == NULL) { CONS_Debug(DBG_GAMELOGIC, "Pathfindsetup has NULL startnodedata.\n"); } else if (pathfindsetup->endnodedata == NULL) { CONS_Debug(DBG_GAMELOGIC, "Pathfindsetup has NULL endnodedata.\n"); } else if (pathfindsetup->getconnectednodes == NULL) { CONS_Debug(DBG_GAMELOGIC, "Pathfindsetup has NULL getconnectednodes function.\n"); } else if (pathfindsetup->getconnectioncosts == NULL) { CONS_Debug(DBG_GAMELOGIC, "Pathfindsetup has NULL getconnectioncosts function.\n"); } else if (pathfindsetup->getheuristic == NULL) { CONS_Debug(DBG_GAMELOGIC, "Pathfindsetup has NULL getheuristic function.\n"); } else if (pathfindsetup->gettraversable == NULL) { CONS_Debug(DBG_GAMELOGIC, "Pathfindsetup has NULL gettraversable function.\n"); } else if (pathfindsetup->getconnectednodes(pathfindsetup->startnodedata, &sourcenodenumconnectednodes) == NULL) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindSetupValid: Source node returned NULL connecting nodes.\n"); } else if (sourcenodenumconnectednodes == 0U) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindSetupValid: Source node has 0 connecting nodes.\n"); } else if (pathfindsetup->getconnectednodes(pathfindsetup->endnodedata, &endnodenumconnectednodes) == NULL) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindSetupValid: End node returned NULL connecting nodes.\n"); } else if (endnodenumconnectednodes == 0U) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindSetupValid: End node has 0 connecting nodes.\n"); } else { pathfindsetupvalid = true; } return pathfindsetupvalid; } static boolean K_ReconstructPath(path_t *const path, pathfindnode_t *const destinationnode) { boolean reconstructsuccess = false; I_Assert(path != NULL); I_Assert(destinationnode != NULL); { size_t numnodes = 0U; pathfindnode_t *thisnode = destinationnode; // If the path we're placing our new path into already has data, free it if (path->array != NULL) { Z_Free(path->array); path->numnodes = 0U; path->totaldist = 0U; } // Do a fast check of how many nodes there are so we know how much space to allocate for (thisnode = destinationnode; thisnode; thisnode = thisnode->camefrom) { numnodes++; } if (numnodes > 0U) { // Allocate memory for the path path->numnodes = numnodes; path->array = Z_Calloc(numnodes * sizeof(pathfindnode_t), PU_STATIC, NULL); path->totaldist = destinationnode->gscore; if (path->array == NULL) { I_Error("K_ReconstructPath: Out of memory."); } // Put the nodes into the return array for (thisnode = destinationnode; thisnode; thisnode = thisnode->camefrom) { path->array[numnodes - 1U] = *thisnode; // Correct the camefrom element to point to the previous element in the array instead if ((path->array[numnodes - 1U].camefrom != NULL) && (numnodes > 1U)) { path->array[numnodes - 1U].camefrom = &path->array[numnodes - 2U]; } else { path->array[numnodes - 1U].camefrom = NULL; } numnodes--; } reconstructsuccess = true; } } return reconstructsuccess; } /*-------------------------------------------------- boolean K_PathfindAStar(path_t *const path, pathfindsetup_t *const pathfindsetup) See header file for description. --------------------------------------------------*/ boolean K_PathfindAStar(path_t *const path, pathfindsetup_t *const pathfindsetup) { boolean pathfindsuccess = false; if (path == NULL) { CONS_Debug(DBG_GAMELOGIC, "NULL path in K_PathfindAStar.\n"); } else if (pathfindsetup == NULL) { CONS_Debug(DBG_GAMELOGIC, "NULL pathfindsetup in K_PathfindAStar.\n"); } else if (!K_PathfindSetupValid(pathfindsetup)) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindAStar: Pathfinding setup is not valid.\n"); } else if (pathfindsetup->startnodedata == pathfindsetup->endnodedata) { // At the destination, return a simple 1 node path pathfindnode_t singlenode = {}; singlenode.camefrom = NULL; singlenode.nodedata = pathfindsetup->endnodedata; singlenode.heapindex = SIZE_MAX; singlenode.hscore = 0U; singlenode.gscore = 0U; K_ReconstructPath(path, &singlenode); pathfindsuccess = true; } else { bheap_t openset = {}; bheapitem_t poppedbheapitem = {}; pathfindnode_t *nodesarray = NULL; pathfindnode_t **closedset = NULL; pathfindnode_t *newnode = NULL; pathfindnode_t *currentnode = NULL; pathfindnode_t *connectingnode = NULL; void **connectingnodesdata = NULL; void *checknodedata = NULL; UINT32 *connectingnodecosts = NULL; size_t numconnectingnodes = 0U; size_t connectingnodeheapindex = 0U; size_t nodesarraycount = 0U; size_t closedsetcount = 0U; size_t i = 0U; UINT32 tentativegscore = 0U; // Set the dynamic structure capacites to defaults if they are 0 if (pathfindsetup->nodesarraycapacity == 0U) { pathfindsetup->nodesarraycapacity = DEFAULT_NODEARRAY_CAPACITY; } if (pathfindsetup->opensetcapacity == 0U) { pathfindsetup->opensetcapacity = DEFAULT_OPENSET_CAPACITY; } if (pathfindsetup->closedsetcapacity == 0U) { pathfindsetup->closedsetcapacity = DEFAULT_CLOSEDSET_CAPACITY; } // Allocate the necessary memory nodesarray = Z_Calloc(pathfindsetup->nodesarraycapacity * sizeof(pathfindnode_t), PU_STATIC, NULL); if (nodesarray == NULL) { I_Error("K_PathfindAStar: Out of memory allocating nodes array."); } closedset = Z_Calloc(pathfindsetup->closedsetcapacity * sizeof(pathfindnode_t*), PU_STATIC, NULL); if (closedset == NULL) { I_Error("K_PathfindAStar: Out of memory allocating closed set."); } K_BHeapInit(&openset, pathfindsetup->opensetcapacity); // Create the first node and add it to the open set newnode = &nodesarray[nodesarraycount]; newnode->heapindex = SIZE_MAX; newnode->nodedata = pathfindsetup->startnodedata; newnode->camefrom = NULL; newnode->gscore = 0U; newnode->hscore = pathfindsetup->getheuristic(newnode->nodedata, pathfindsetup->endnodedata); nodesarraycount++; K_BHeapPush(&openset, newnode, K_NodeGetFScore(newnode), K_NodeUpdateHeapIndex); // update openset capacity if it changed if (openset.capacity != pathfindsetup->opensetcapacity) { pathfindsetup->opensetcapacity = openset.capacity; } // Go through each node in the openset, adding new ones from each node to it // this continues until a path is found or there are no more nodes to check while (openset.count > 0U) { // pop the best node off of the openset K_BHeapPop(&openset, &poppedbheapitem); currentnode = (pathfindnode_t*)poppedbheapitem.data; if (currentnode->nodedata == pathfindsetup->endnodedata) { pathfindsuccess = K_ReconstructPath(path, currentnode); break; } // Place the node we just popped into the closed set, as we are now evaluating it if (closedsetcount >= pathfindsetup->closedsetcapacity) { // Need to reallocate closedset to fit another node pathfindsetup->closedsetcapacity = pathfindsetup->closedsetcapacity * 2; closedset = Z_Realloc(closedset, pathfindsetup->closedsetcapacity * sizeof(pathfindnode_t*), PU_STATIC, NULL); if (closedset == NULL) { I_Error("K_PathfindAStar: Out of memory reallocating closed set."); } } closedset[closedsetcount] = currentnode; closedsetcount++; // Get the needed data for the next nodes from the current node connectingnodesdata = pathfindsetup->getconnectednodes(currentnode->nodedata, &numconnectingnodes); connectingnodecosts = pathfindsetup->getconnectioncosts(currentnode->nodedata); if (connectingnodesdata == NULL) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindAStar: A Node returned NULL connecting node data.\n"); } else if (connectingnodecosts == NULL) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindAStar: A Node returned NULL connecting node costs.\n"); } else { // For each connecting node add it to the openset if it's unevaluated and not there, // skip it if it's in the closedset or not traversable for (i = 0; i < numconnectingnodes; i++) { checknodedata = connectingnodesdata[i]; if (checknodedata == NULL) { CONS_Debug(DBG_GAMELOGIC, "K_PathfindAStar: A Node has a NULL connecting node.\n"); } else { // skip this node if it isn't traversable if (pathfindsetup->gettraversable(checknodedata) == false) { continue; } // Figure out what the gscore of this route for the connecting node is tentativegscore = currentnode->gscore + connectingnodecosts[i]; // find this data in the nodes array if it's been generated before connectingnode = K_NodesArrayContainsNodeData(nodesarray, checknodedata, nodesarraycount); if (connectingnode != NULL) { // The connecting node has been seen before, so it must be in either the closedset (skip it) // or the openset (re-evaluate it's gscore) if (K_ClosedsetContainsNode(closedset, connectingnode, closedsetcount) == true) { continue; } else if (tentativegscore < connectingnode->gscore) { // The node is not in the closedset, update it's gscore if this path to it is faster connectingnode->gscore = tentativegscore; connectingnode->camefrom = currentnode; connectingnodeheapindex = K_BHeapContains(&openset, connectingnode, connectingnode->heapindex); if (connectingnodeheapindex != SIZE_MAX) { K_UpdateBHeapItemValue( &openset.array[connectingnodeheapindex], K_NodeGetFScore(connectingnode)); } else { // SOMEHOW the node is not in either the closed set OR the open set CONS_Debug(DBG_GAMELOGIC, "K_PathfindAStar: A Node is not in either set.\n"); } } } else { // Node is not created yet, so it hasn't been seen so far // Reallocate nodesarray if it's full if (nodesarraycount >= pathfindsetup->nodesarraycapacity) { pathfindnode_t *nodesarrayrealloc = NULL; pathfindsetup->nodesarraycapacity = pathfindsetup->nodesarraycapacity * 2; nodesarrayrealloc = Z_Realloc(nodesarray, pathfindsetup->nodesarraycapacity * sizeof(pathfindnode_t), PU_STATIC, NULL); if (nodesarrayrealloc == NULL) { I_Error("K_PathfindAStar: Out of memory reallocating nodes array."); } // Need to update pointers in closedset, openset, and node "camefrom" if nodesarray moved. if (nodesarray != nodesarrayrealloc) { size_t j = 0U; size_t arrayindex = 0U; for (j = 0U; j < closedsetcount; j++) { arrayindex = closedset[j] - nodesarray; closedset[j] = &nodesarrayrealloc[arrayindex]; } for (j = 0U; j < openset.count; j++) { arrayindex = ((pathfindnode_t *)(openset.array[j].data)) - nodesarray; openset.array[j].data = &nodesarrayrealloc[arrayindex]; } for (j = 0U; j < nodesarraycount; j++) { if (nodesarrayrealloc[j].camefrom != NULL) { arrayindex = nodesarrayrealloc[j].camefrom - nodesarray; nodesarrayrealloc[j].camefrom = &nodesarrayrealloc[arrayindex]; } } arrayindex = currentnode - nodesarray; currentnode = &nodesarrayrealloc[arrayindex]; } nodesarray = nodesarrayrealloc; } // Create the new node and add it to the nodes array and open set newnode = &nodesarray[nodesarraycount]; newnode->heapindex = SIZE_MAX; newnode->nodedata = checknodedata; newnode->camefrom = currentnode; newnode->gscore = tentativegscore; newnode->hscore = pathfindsetup->getheuristic(newnode->nodedata, pathfindsetup->endnodedata); nodesarraycount++; K_BHeapPush(&openset, newnode, K_NodeGetFScore(newnode), K_NodeUpdateHeapIndex); } } } } } // Clean up the memory K_BHeapFree(&openset); Z_Free(closedset); Z_Free(nodesarray); } return pathfindsuccess; }