From 1bfd3517dd85e7685f0c75e484f8c2b446a354d5 Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:42:08 +0200 Subject: [PATCH 01/10] refactor(pathfinder): Move PathNode implementation to its own file (#3429) --- Core/GameEngine/CMakeLists.txt | 1 + .../Source/GameLogic/AI/AIPathfind.cpp | 101 --------------- .../GameLogic/AI/Pathfinder/PathNode.cpp | 119 ++++++++++++++++++ 3 files changed, 120 insertions(+), 101 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathNode.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index e29b9f93290..49b3c189547 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -862,6 +862,7 @@ set(GAMEENGINE_SRC # Source/GameLogic/AI/AISkirmishPlayer.cpp # Source/GameLogic/AI/AIStates.cpp # Source/GameLogic/AI/AITNGuard.cpp + Source/GameLogic/AI/Pathfinder/PathNode.cpp # Source/GameLogic/AI/Squad.cpp # Source/GameLogic/AI/TurretAI.cpp Source/GameLogic/Map/PolygonTrigger.cpp diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index e32fe84caf3..4d6cfd61160 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -119,107 +119,6 @@ constexpr const UnsignedInt MAX_SAFE_PATH_CELL_COUNT = 2000; constexpr const UnsignedInt PATHFIND_CELLS_PER_FRAME = 5000; // Number of cells we will search pathfinding per frame. constexpr const UnsignedInt CELL_INFOS_TO_ALLOCATE = 30000; -//----------------------------------------------------------------------------------- -PathNode::PathNode() : - m_nextOpti(nullptr), - m_next(nullptr), - m_prev(nullptr), - m_nextOptiDist2D(0), - m_canOptimize(false), - m_id(-1) -{ - m_nextOptiDirNorm2D.x = 0; - m_nextOptiDirNorm2D.y = 0; - m_pos.zero(); - m_layer = LAYER_INVALID; -} - -//----------------------------------------------------------------------------------- -PathNode::~PathNode() -{ -} - -//----------------------------------------------------------------------------------- -void PathNode::setNextOptimized(PathNode *node) -{ - m_nextOpti = node; - if (node) - { - m_nextOptiDirNorm2D.x = node->getPosition()->x - getPosition()->x; - m_nextOptiDirNorm2D.y = node->getPosition()->y - getPosition()->y; - m_nextOptiDist2D = m_nextOptiDirNorm2D.length(); - if (m_nextOptiDist2D == 0.0f) - { - //DEBUG_LOG(("Warning - Path Seg length == 0, adjusting. john a.")); - m_nextOptiDist2D = 0.01f; - } - m_nextOptiDirNorm2D.x /= m_nextOptiDist2D; - m_nextOptiDirNorm2D.y /= m_nextOptiDist2D; - } - else - { - m_nextOptiDist2D = 0; - } -} - -//----------------------------------------------------------------------------------- -/// given a list, prepend this node, return new list -PathNode *PathNode::prependToList( PathNode *list ) -{ - m_next = list; - if (list) - list->m_prev = this; - m_prev = nullptr; - return this; -} - - -//----------------------------------------------------------------------------------- -/// given a node, append new node to this. -void PathNode::append( PathNode *newNode ) -{ - newNode->m_next = this->m_next; - newNode->m_prev = this; - if (newNode->m_next) { - newNode->m_next->m_prev = newNode; - } - this->m_next = newNode; - -} - -//----------------------------------------------------------------------------------- -/** - * Compute direction vector to next node - */ -const Coord3D *PathNode::computeDirectionVector() -{ - static Coord3D dir; - - if (m_next == nullptr) - { - if (m_prev == nullptr) - { - // only one node on whole path - no direction - dir.x = 0.0f; - dir.y = 0.0f; - dir.z = 0.0f; - } - else - { - // tail node - continue prior direction - return m_prev->computeDirectionVector(); - } - } - else - { - dir.x = m_next->m_pos.x - m_pos.x; - dir.y = m_next->m_pos.y - m_pos.y; - dir.z = m_next->m_pos.z - m_pos.z; - } - - return &dir; -} - //----------------------------------------------------------------------------------- Path::Path(): diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathNode.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathNode.cpp new file mode 100644 index 00000000000..a0ec8bcb4a4 --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathNode.cpp @@ -0,0 +1,119 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/Pathfinder/PathNode.h" + +PathNode::PathNode() : + m_nextOpti(nullptr), + m_next(nullptr), + m_prev(nullptr), + m_nextOptiDist2D(0), + m_canOptimize(false), + m_id(-1) +{ + m_nextOptiDirNorm2D.x = 0; + m_nextOptiDirNorm2D.y = 0; + m_pos.zero(); + m_layer = LAYER_INVALID; +} + +//----------------------------------------------------------------------------------- +PathNode::~PathNode() +{ +} + +//----------------------------------------------------------------------------------- +void PathNode::setNextOptimized(PathNode *node) +{ + m_nextOpti = node; + if (node) + { + m_nextOptiDirNorm2D.x = node->getPosition()->x - getPosition()->x; + m_nextOptiDirNorm2D.y = node->getPosition()->y - getPosition()->y; + m_nextOptiDist2D = m_nextOptiDirNorm2D.length(); + if (m_nextOptiDist2D == 0.0f) + { + //DEBUG_LOG(("Warning - Path Seg length == 0, adjusting. john a.")); + m_nextOptiDist2D = 0.01f; + } + m_nextOptiDirNorm2D.x /= m_nextOptiDist2D; + m_nextOptiDirNorm2D.y /= m_nextOptiDist2D; + } + else + { + m_nextOptiDist2D = 0; + } +} + +//----------------------------------------------------------------------------------- +/// given a list, prepend this node, return new list +PathNode *PathNode::prependToList( PathNode *list ) +{ + m_next = list; + if (list) + list->m_prev = this; + m_prev = nullptr; + return this; +} + + +//----------------------------------------------------------------------------------- +/// given a node, append new node to this. +void PathNode::append( PathNode *newNode ) +{ + newNode->m_next = this->m_next; + newNode->m_prev = this; + if (newNode->m_next) { + newNode->m_next->m_prev = newNode; + } + this->m_next = newNode; + +} + +//----------------------------------------------------------------------------------- +/** + * Compute direction vector to next node + */ +const Coord3D *PathNode::computeDirectionVector() +{ + static Coord3D dir; + + if (m_next == nullptr) + { + if (m_prev == nullptr) + { + // only one node on whole path - no direction + dir.x = 0.0f; + dir.y = 0.0f; + dir.z = 0.0f; + } + else + { + // tail node - continue prior direction + return m_prev->computeDirectionVector(); + } + } + else + { + dir.x = m_next->m_pos.x - m_pos.x; + dir.y = m_next->m_pos.y - m_pos.y; + dir.z = m_next->m_pos.z - m_pos.z; + } + + return &dir; +} From 0b2fbf3c2d17cc75a4db3932dd931c9e228b8dc7 Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:55:35 +0200 Subject: [PATCH 02/10] refactor(pathfinder): Move Path implementation to its own file (#3429) --- Core/GameEngine/CMakeLists.txt | 1 + .../Source/GameLogic/AI/AIPathfind.cpp | 850 ----------------- .../Source/GameLogic/AI/Pathfinder/Path.cpp | 878 ++++++++++++++++++ 3 files changed, 879 insertions(+), 850 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/Path.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 49b3c189547..b69e48170d2 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -862,6 +862,7 @@ set(GAMEENGINE_SRC # Source/GameLogic/AI/AISkirmishPlayer.cpp # Source/GameLogic/AI/AIStates.cpp # Source/GameLogic/AI/AITNGuard.cpp + Source/GameLogic/AI/Pathfinder/Path.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp # Source/GameLogic/AI/Squad.cpp # Source/GameLogic/AI/TurretAI.cpp diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 4d6cfd61160..d463053cbac 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -119,856 +119,6 @@ constexpr const UnsignedInt MAX_SAFE_PATH_CELL_COUNT = 2000; constexpr const UnsignedInt PATHFIND_CELLS_PER_FRAME = 5000; // Number of cells we will search pathfinding per frame. constexpr const UnsignedInt CELL_INFOS_TO_ALLOCATE = 30000; - -//----------------------------------------------------------------------------------- -Path::Path(): -m_path(nullptr), -m_pathTail(nullptr), -m_isOptimized(FALSE), -m_blockedByAlly(FALSE), -m_cpopRecentStart(nullptr), -m_cpopCountdown(MAX_CPOP), -m_cpopValid(FALSE) -{ - m_cpopIn.zero(); - m_cpopOut.distAlongPath=0; - m_cpopOut.layer = LAYER_GROUND; - m_cpopOut.posOnPath.zero(); -} - -Path::~Path() -{ - PathNode *node, *nextNode; - - // delete all of the path nodes - for( node = m_path; node; node = nextNode ) - { - nextNode = node->getNext(); - deleteInstance(node); - } -} - -// ------------------------------------------------------------------------------------------------ -/** CRC */ -// ------------------------------------------------------------------------------------------------ -void Path::crc( Xfer *xfer ) -{ -} - -// ------------------------------------------------------------------------------------------------ -/** Xfer Method */ -// ------------------------------------------------------------------------------------------------ -void Path::xfer( Xfer *xfer ) -{ - // version - XferVersion currentVersion = 1; - XferVersion version = currentVersion; - xfer->xferVersion( &version, currentVersion ); - - PathNode *node = m_path; - Int count = 0; - while (node) { - count++; - node = node->getNext(); - } - xfer->xferInt(&count); - - if (xfer->getXferMode() == XFER_SAVE) { - node = m_pathTail; // Write them out backwards. - while (node) { - node->m_id = count; - xfer->xferInt(&count); - Coord3D pos = *node->getPosition(); - xfer->xferCoord3D(&pos); - PathfindLayerEnum layer = node->getLayer(); - xfer->xferUser(&layer, sizeof(layer)); - Bool canOpt = node->getCanOptimize(); - xfer->xferBool(&canOpt); - Int id = -1; - if (node->getNextOptimized()) { - id = node->getNextOptimized()->m_id; - } - xfer->xferInt(&id); - count--; - node = node->getPrevious(); - } - DEBUG_ASSERTCRASH(count==0, ("Wrong data count")); - } else { - m_cpopValid = FALSE; - while (count) { - Int nodeId; - xfer->xferInt(&nodeId); - DEBUG_ASSERTCRASH(nodeId==count, ("Bad data")); - Coord3D pos; - xfer->xferCoord3D(&pos); - PathfindLayerEnum layer; - xfer->xferUser(&layer, sizeof(layer)); - Bool canOpt; - xfer->xferBool(&canOpt); - Int optID = -1; - xfer->xferInt(&optID); - PathNode *node = newInstance(PathNode); - node->m_id = nodeId; - node->setPosition(&pos); - node->setLayer(layer); - node->setCanOptimize(canOpt); - PathNode *optNode = nullptr; - if (optID > 0) { - optNode = m_path; - while (optNode && optNode->m_id != optID) { - optNode = optNode->getNext(); - } - DEBUG_ASSERTCRASH (optNode && optNode->m_id == optID, ("Could not find optimized link.")); - } - m_path = node->prependToList(m_path); - if (m_pathTail == nullptr) - m_pathTail = node; - if (optNode) { - node->setNextOptimized(optNode); - } - count--; - } - } - - xfer->xferBool(&m_isOptimized); - Int obsolete1 = 0; - xfer->xferInt(&obsolete1); - UnsignedInt obsolete2; - xfer->xferUnsignedInt(&obsolete2); - xfer->xferBool(&m_blockedByAlly); - - -#if defined(RTS_DEBUG) - if (TheGlobalData->m_debugAI == AI_DEBUG_PATHS) - { - extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); - RGBColor color; - color.blue = 0; - color.red = color.green = 1; - Coord3D pos; - addIcon(nullptr, 0, 0, color); // erase feedback. - for( PathNode *node = getFirstNode(); node; node = node->getNext() ) - { - - // create objects to show path - they decay - - pos = *node->getPosition(); - addIcon(&pos, PATHFIND_CELL_SIZE_F*.25f, 200, color); - } - - // show optimized path - for( node = getFirstNode(); node; node = node->getNextOptimized() ) - { - pos = *node->getPosition(); - addIcon(&pos, PATHFIND_CELL_SIZE_F*.8f, 200, color); - } - TheAI->pathfinder()->setDebugPath(this); - } -#endif -} - -// ------------------------------------------------------------------------------------------------ -/** Load post process */ -// ------------------------------------------------------------------------------------------------ -void Path::loadPostProcess() -{ -} - -/** - * Create a new node at the head of the path - */ -void Path::prependNode( const Coord3D *pos, PathfindLayerEnum layer ) -{ - PathNode *node = newInstance(PathNode); - - node->setPosition( pos ); - node->setLayer(layer); - - m_path = node->prependToList( m_path ); - - if (m_pathTail == nullptr) - m_pathTail = node; - - m_isOptimized = false; -} - -/** - * Create a new node at the tail of the path - */ -void Path::appendNode( const Coord3D *pos, PathfindLayerEnum layer ) -{ - if (m_isOptimized && m_pathTail) - { - /* Check for duplicates. */ - if (pos->x == m_pathTail->getPosition()->x && pos->y == m_pathTail->getPosition()->y) { - DEBUG_LOG(("Warning - Path Seg length == 0, ignoring. john a.")); - return; - } - } - PathNode *node = newInstance(PathNode); - - node->setPosition( pos ); - node->setLayer(layer); - - if (!m_path) - { - m_path = node; - m_pathTail = node; - - return; - } - - m_pathTail->append(node); - - if (m_isOptimized) - { - m_pathTail->setNextOptimized(node); - } - - m_pathTail = node; -} -/** - * Create a new node at the tail of the path - */ -void Path::updateLastNode( const Coord3D *pos ) -{ - PathfindLayerEnum layer = TheTerrainLogic->getLayerForDestination(pos); - if (m_pathTail) { - m_pathTail->setPosition(pos); - m_pathTail->setLayer(layer); - } - if (m_isOptimized && m_pathTail) - { - PathNode *node = m_path; - while(node && node->getNextOptimized() != m_pathTail) { - node = node->getNextOptimized(); - } - if (node && node->getNextOptimized() == m_pathTail) { - node->setNextOptimized(m_pathTail); - } - } -} - -/** - * Optimize the path by checking line of sight - */ -void Path::optimize( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, Bool blocked ) -{ - PathNode *node, *anchor; - - // start with first node in the path - anchor = getFirstNode(); - - Bool firstNode = true; - PathfindLayerEnum firstLayer = anchor->getLayer(); - - // backwards. - - // - // For each node in the path, check LOS from last node in path, working forward. - // When a clear LOS is found, keep the resulting straight line segment. - // - while( anchor != getLastNode() ) - { - // find the farthest node in the path that has a clear line-of-sight to this anchor - Bool optimizedSegment = false; - PathfindLayerEnum layer = anchor->getLayer(); - PathfindLayerEnum curLayer = anchor->getLayer(); - Int count = 0; - const Int ALLOWED_STEPS = 3; // we can optimize 3 steps to or from a bridge. Otherwise, we need to insert a point. jba. - for (node = anchor->getNext(); node->getNext(); node=node->getNext()) { - count++; - if (curLayer==LAYER_GROUND) { - if (node->getLayer() != curLayer) { - layer = node->getLayer(); - curLayer = layer; - if (count > ALLOWED_STEPS) break; - } - } else { - if (node->getNext()->getLayer() != curLayer) { - if (count > ALLOWED_STEPS) break; - } - } - curLayer = node->getLayer(); - if (node->getCanOptimize()==false) { - break; - } - } - if (firstNode) { - layer = firstLayer; - firstNode = false; - } - //PathfindLayerEnum curLayer = LAYER_GROUND; - for( ; node != anchor; node = node->getPrevious() ) - { - Bool isPassable = false; - //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); - if (TheAI->pathfinder()->isLinePassable( obj, acceptableSurfaces, layer, *anchor->getPosition(), - *node->getPosition(), blocked, false)) - { - isPassable = true; - } - PathfindCell* cell = TheAI->pathfinder()->getCell( layer, node->getPosition()); - if (cell && cell->getType()==PathfindCell::CELL_CLIFF && !cell->getPinched()) { - isPassable = true; - } - // Horizontal, diagonal, and vertical steps are passable. - if (!isPassable) { - Int dx = node->getPosition()->x - anchor->getPosition()->x; - Int dy = node->getPosition()->y - anchor->getPosition()->y; - Bool mightBePassable = false; - if (IABS(dx)==PATHFIND_CELL_SIZE && IABS(dy)==PATHFIND_CELL_SIZE) { - isPassable = true; - } - PathNode *tmpNode; - if (dx==0) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; - if (dx!=0) mightBePassable = false; - } - } - if (dy==0) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; - if (dy!=0) mightBePassable = false; - } - } - if (dx == dy) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; - dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; - if (dy!=dx) mightBePassable = false; - } - } - if (dx == -dy) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; - dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; - if (dy!=-dx) mightBePassable = false; - } - } - if (mightBePassable) { - isPassable = true; - } - } - if (isPassable) - { - // anchor can directly see this node, make it next in the optimized path - anchor->setNextOptimized( node ); - anchor = node; - optimizedSegment = true; - break; - } - } - - if (optimizedSegment == false) - { - // for some reason, there is no clear LOS between the anchor node and the very next node - anchor->setNextOptimized( anchor->getNext() ); - anchor = anchor->getNext(); - } - } - - // the path has been optimized - m_isOptimized = true; -} - -/** - * Optimize the path by checking line of sight - */ -void Path::optimizeGroundPath( Bool crusher, Int pathDiameter ) -{ - PathNode *node, *anchor; - - // start with first node in the path - anchor = getFirstNode(); - - // - // For each node in the path, check LOS from last node in path, working forward. - // When a clear LOS is found, keep the resulting straight line segment. - // - while( anchor != getLastNode() ) - { - // find the farthest node in the path that has a clear line-of-sight to this anchor - Bool optimizedSegment = false; - PathfindLayerEnum layer = anchor->getLayer(); - PathfindLayerEnum curLayer = anchor->getLayer(); - Int count = 0; - const Int ALLOWED_STEPS = 3; // we can optimize 3 steps to or from a bridge. Otherwise, we need to insert a point. jba. - for (node = anchor->getNext(); node->getNext(); node=node->getNext()) { - count++; - if (curLayer==LAYER_GROUND) { - if (node->getLayer() != curLayer) { - layer = node->getLayer(); - curLayer = layer; - if (count > ALLOWED_STEPS) break; - } - } else { - if (node->getNext()->getLayer() != curLayer) { - if (count > ALLOWED_STEPS) break; - } - } - curLayer = node->getLayer(); - } - - // find the farthest node in the path that has a clear line-of-sight to this anchor - for( ; node != anchor; node = node->getPrevious() ) - { - Bool isPassable = false; - //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); - if (TheAI->pathfinder()->isGroundPathPassable( crusher, *anchor->getPosition(), layer, - *node->getPosition(), pathDiameter)) - { - isPassable = true; - } - // Horizontal, diagonal, and vertical steps are passable. - if (!isPassable) { - Int dx = node->getPosition()->x - anchor->getPosition()->x; - Int dy = node->getPosition()->y - anchor->getPosition()->y; - Bool mightBePassable = false; - PathNode *tmpNode; - if (dx==0) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; - if (dx!=0) mightBePassable = false; - } - } - if (dy==0) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; - if (dy!=0) mightBePassable = false; - } - } - if (dx == dy) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; - dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; - if (dy!=dx) mightBePassable = false; - } - } - if (dx == -dy) { - mightBePassable = true; - for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { - dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; - dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; - if (dy!=-dx) mightBePassable = false; - } - } - if (mightBePassable) { - isPassable = true; - } - } - if (isPassable) - { - // anchor can directly see this node, make it next in the optimized path - anchor->setNextOptimized( node ); - anchor = node; - optimizedSegment = true; - break; - } - } - - if (optimizedSegment == false) - { - // for some reason, there is no clear LOS between the anchor node and the very next node - anchor->setNextOptimized( anchor->getNext() ); - anchor = anchor->getNext(); - } - } - - // Remove jig/jogs :) jba. - for (anchor=getFirstNode(); anchor!=nullptr; anchor=anchor->getNextOptimized()) { - node = anchor->getNextOptimized(); - if (node && node->getNextOptimized()) { - Real dx = node->getPosition()->x - anchor->getPosition()->x; - Real dy = node->getPosition()->y - anchor->getPosition()->y; - // If the x & y offsets are less than 2 pathfind cells, kill it. - if (dx*dx+dy*dy < sqr(PATHFIND_CELL_SIZE_F)*3.9f) { - anchor->setNextOptimized(node->getNextOptimized()); - } - } - } - - // the path has been optimized - m_isOptimized = true; -} - -inline Bool isReallyClose(const Coord3D& a, const Coord3D& b) -{ - const Real CLOSE_ENOUGH = 0.1f; - return - fabs(a.x-b.x) <= CLOSE_ENOUGH && - fabs(a.y-b.y) <= CLOSE_ENOUGH && - fabs(a.z-b.z) <= CLOSE_ENOUGH; -} - -/** - * Given a location, return the closest position on the path. - * If 'allowBacktrack' is true, the entire path is considered. - * If it is false, the point computed cannot be prior to previously returned non-backtracking points on this path. - * Because the path "knows" the direction of travel, it will "lead" the given position a bit - * to ensure the path is followed in the intended direction. - * - * Note: The path cleanup does not take into account rolling terrain, so we can end up with - * these situations: - * - * B - * ###### - * ########## - * A-##----------##---C - * ####################### - * - * - * When an agent gets to B, he seems far off of the path, but it really not. - * There are similar problems with valleys. - * - * Since agents track the closest path, if a high hill gets close to the underside of - * a bridge, an agent may 'jump' to the higher path. This must be avoided in maps. - * - * return along-path distance to the end will be returned as function result - */ -void Path::computePointOnPath( - const Object* obj, - const LocomotorSet& locomotorSet, - const Coord3D& pos, - ClosestPointOnPathInfo& out -) -{ - CRCDEBUG_LOG(("Path::computePointOnPath() for %s", DebugDescribeObject(obj).str())); - - out.layer = LAYER_GROUND; - out.posOnPath.zero(); - out.distAlongPath = 0; - - if (m_path == nullptr) - { - m_cpopValid = false; - return; - } - out.layer = m_path->getLayer(); - - if (m_cpopValid && m_cpopCountdown>0 && isReallyClose(pos, m_cpopIn)) - { - out = m_cpopOut; - m_cpopCountdown--; - CRCDEBUG_LOG(("Path::computePointOnPath() end because we're really close")); - return; - } - m_cpopCountdown = MAX_CPOP; - - // default pathPos to end of the path - out.posOnPath = *getLastNode()->getPosition(); - - const PathNode* closeNode = nullptr; - Coord2D toPos; - Real closeDistSqr = 99999999.9f; - Real totalPathLength = 0.0f; - Real lengthAlongPathToPos = 0.0f; - - // - // Find the closest segment of the path - // - const PathNode* prevNode = m_path; - Coord2D segmentDirNorm; - Real segmentLength; - - // note that the seg dir and len returned by this is the dist & vec from 'prevNode' to 'node' - for ( const PathNode* node = prevNode->getNextOptimized(&segmentDirNorm, &segmentLength); - node != nullptr; - node = node->getNextOptimized(&segmentDirNorm, &segmentLength) ) - { - const Coord3D* prevNodePos = prevNode->getPosition(); - const Coord3D* nodePos = node->getPosition(); - - // compute vector from start of segment to pos - toPos.x = pos.x - prevNodePos->x; - toPos.y = pos.y - prevNodePos->y; - - // compute distance projection of 'toPos' onto segment - Real alongPathDist = segmentDirNorm.x * toPos.x + segmentDirNorm.y * toPos.y; - - Coord3D pointOnPath; - if (alongPathDist < 0.0f) - { - // projected point is before start of segment, use starting point - alongPathDist = 0.0f; - pointOnPath = *prevNodePos; - } - else if (alongPathDist > segmentLength) - { - // projected point is beyond end of segment, use end point - if (node->getNextOptimized() == nullptr) - { - alongPathDist = segmentLength; - pointOnPath = *nodePos; - } - else - { - // beyond the end of this segment, skip this segment - // if bend is sharp, start of next segment will grab this point - // if bend is gradual, the point will project into the next segment - totalPathLength += segmentLength; - prevNode = node; - continue; - } - } - else - { - // projected point is on this segment, compute it - pointOnPath.x = prevNodePos->x + alongPathDist * segmentDirNorm.x; - pointOnPath.y = prevNodePos->y + alongPathDist * segmentDirNorm.y; - pointOnPath.z = 0; - } - - // compute distance to point on path, and track the closest we've found so far - Coord2D offset; - offset.x = pos.x - pointOnPath.x; - offset.y = pos.y - pointOnPath.y; - - Real offsetDistSqr = offset.x*offset.x + offset.y*offset.y; - if (offsetDistSqr < closeDistSqr) - { - closeDistSqr = offsetDistSqr; - closeNode = prevNode; - out.posOnPath = pointOnPath; - - lengthAlongPathToPos = totalPathLength + alongPathDist; - } - - // add this segment's length to find total path length - /// @todo Precompute this and store in path - totalPathLength += segmentLength; - prevNode = node; - DUMPCOORD3D(&pointOnPath); - } - - // - // Compute the goal movement position for this agent - // - if (closeNode && closeNode->getNextOptimized()) - { - // note that the seg dir and len returned by this is the dist & vec from 'closeNode' to 'closeNext' - const PathNode* closeNext = closeNode->getNextOptimized(&segmentDirNorm, &segmentLength); - const Coord3D* nextNodePos = closeNext->getPosition(); - const Coord3D* closeNodePos = closeNode->getPosition(); - - const PathNode* closePrev = closeNode->getPrevious(); - if (closePrev && closePrev->getLayer() > LAYER_GROUND) - { - out.layer = closeNode->getLayer(); - } - if (closeNode->getLayer() > LAYER_GROUND) - { - out.layer = closeNode->getLayer(); - } - - if (closeNext->getLayer() > LAYER_GROUND) - { - out.layer = closeNext->getLayer(); - } - - // compute vector from start of segment to pos - toPos.x = pos.x - closeNodePos->x; - toPos.y = pos.y - closeNodePos->y; - - // compute distance projection of 'toPos' onto segment - Real alongPathDist = segmentDirNorm.x * toPos.x + segmentDirNorm.y * toPos.y; - - // we know this is the closest segment, so don't allow farther back than the start node - if (alongPathDist < 0.0f) - alongPathDist = 0.0f; - - // compute distance of point from this path segment - Real toDistSqr = sqr(toPos.x) + sqr(toPos.y); - Real offsetDistSq = toDistSqr - sqr(alongPathDist); - Real offsetDist = (offsetDistSq <= 0.0) ? 0.0 : sqrt(offsetDistSq); - - // If we are basically on the path, return the next path node as the movement goal. - // However, the farther off the path we get, the movement goal becomes closer to our - // projected position on the path. If we are very far off the path, we will move - // directly towards the nearest point on the path, and not the next path node. - const Real maxPathError = 3.0f * PATHFIND_CELL_SIZE_F; - const Real maxPathErrorInv = 1.0 / maxPathError; - Real k = offsetDist * maxPathErrorInv; - if (k > 1.0f) - k = 1.0f; - - Bool gotPos = false; - CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 1")); - if (TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), out.layer, pos, *nextNodePos, - false, true )) - { - out.posOnPath = *nextNodePos; - gotPos = true; - - Bool tryAhead = alongPathDist > segmentLength * 0.5; - if (closeNext->getCanOptimize() == false) - { - tryAhead = false; // don't go past no-opt nodes. - } - if (closeNode->getLayer() != closeNext->getLayer()) - { - tryAhead = false; // don't go past layers. - } - if (obj->getLayer()!=LAYER_GROUND) { - tryAhead = false; - } - Bool veryClose = false; - if (segmentLength-alongPathDist<1.0f) { - tryAhead = true; - veryClose = true; - } - if (tryAhead) - { - // try next segment middle. - const PathNode *next = closeNext->getNextOptimized(); - if (next) - { - Coord3D tryPos; - tryPos.x = (nextNodePos->x + next->getPosition()->x) * 0.5; - tryPos.y = (nextNodePos->y + next->getPosition()->y) * 0.5; - tryPos.z = nextNodePos->z; - CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 2")); - if (veryClose || TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), closeNext->getLayer(), pos, tryPos, false, true )) - { - gotPos = true; - out.posOnPath = tryPos; - } - } - } - } - else if (k > 0.5f) - { - Real tryDist = alongPathDist + (0.5) * (segmentLength - alongPathDist); - - // projected point is on this segment, compute it - out.posOnPath.x = closeNodePos->x + tryDist * segmentDirNorm.x; - out.posOnPath.y = closeNodePos->y + tryDist * segmentDirNorm.y; - out.posOnPath.z = closeNodePos->z; - - CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 3")); - if (TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), out.layer, pos, out.posOnPath, false, true )) - { - k = 0.5f; - gotPos = true; - } - } - - // if we are on the path (k == 0), then alongPathDist == segmentLength - // if we are way off the path (k == 1), then alongPathDist is unchanged, and it projection of actual pos - alongPathDist += (1.0f - k) * (segmentLength - alongPathDist); - - if (!gotPos) - { - if (alongPathDist > segmentLength) - { - alongPathDist = segmentLength; - out.posOnPath = *nextNodePos; - } - else - { - // projected point is on this segment, compute it - out.posOnPath.x = closeNodePos->x + alongPathDist * segmentDirNorm.x; - out.posOnPath.y = closeNodePos->y + alongPathDist * segmentDirNorm.y; - out.posOnPath.z = closeNodePos->z; - Real dx = fabs(pos.x - out.posOnPath.x); - Real dy = fabs(pos.y - out.posOnPath.y); - if (dx<1 && dy<1 && closeNode->getNextOptimized() && closeNode->getNextOptimized()->getNextOptimized()) { - out.posOnPath = *closeNode->getNextOptimized()->getNextOptimized()->getPosition(); - } - } - } - } - - TheAI->pathfinder()->setDebugPathPosition( &out.posOnPath ); - - out.distAlongPath = totalPathLength - lengthAlongPathToPos; - - Coord3D delta; - delta.x = out.posOnPath.x - pos.x; - delta.y = out.posOnPath.y - pos.y; - delta.z = 0; - Real lenDelta = delta.length(); - if (lenDelta > out.distAlongPath && out.distAlongPath > PATHFIND_CLOSE_ENOUGH) - { - out.distAlongPath = lenDelta; - } - - m_cpopIn = pos; - m_cpopOut = out; - m_cpopValid = true; - CRCDEBUG_LOG(("Path::computePointOnPath() end")); - -} - - -/** - Given a position, computes the distance to the goal. Returns 0 if we are past the goal. - Returns the goal position in goalPos. This is intended for use with flying paths, that go - directly to the goal and don't consider obstacles. jba. - */ -Real Path::computeFlightDistToGoal( const Coord3D *pos, Coord3D& goalPos ) -{ - if (m_path == nullptr) - { - goalPos.x = 0.0f; - goalPos.y = 0.0f; - goalPos.z = 0.0f; - return 0.0f; - } - const PathNode *curNode = getFirstNode(); - if (m_cpopRecentStart) { - curNode = m_cpopRecentStart; - } else { - m_cpopRecentStart = curNode; - } - const PathNode *nextNode = curNode->getNextOptimized(); - goalPos = *curNode->getPosition(); - Real distance = 0; - Bool useNext = true; - while (nextNode) { - - if (useNext) { - goalPos = *nextNode->getPosition(); - } - - Coord3D startPos = *curNode->getPosition(); - Coord3D endPos = *nextNode->getPosition(); - - Coord2D posToGoalVector; - // posToGoalVector is pos to goalPos vector. - posToGoalVector.x = endPos.x - pos->x; - posToGoalVector.y = endPos.y - pos->y; - - // pathVector is the startPos to goal pos vector. - Coord2D pathVector; - pathVector.x = endPos.x - startPos.x; - pathVector.y = endPos.y - startPos.y; - - // Normalize pathVector - pathVector.normalize(); - - // Dot product is the posToGoal vector projected onto the path vector. - Real dotProduct = posToGoalVector.x*pathVector.x + posToGoalVector.y*pathVector.y; - if (dotProduct>=0) { - distance += dotProduct; - useNext = false; - } else if (useNext) { - m_cpopRecentStart = nextNode; - } - curNode = nextNode; - nextNode = curNode->getNextOptimized(); - } - return distance; - -} //----------------------------------------------------------------------------------- PathfindCellInfo *PathfindCellInfo::s_infoArray = nullptr; diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/Path.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/Path.cpp new file mode 100644 index 00000000000..ca4c3c79411 --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/Path.cpp @@ -0,0 +1,878 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "Common/CRCDebug.h" +#include "GameLogic/AI.h" +#include "GameLogic/AIPathfind.h" +#include "GameLogic/Object.h" +#include "GameLogic/Pathfinder/PathfindCell.h" +#include "GameLogic/Pathfinder/Path.h" +#include "GameLogic/Pathfinder/PathNode.h" +#include "GameLogic/TerrainLogic.h" + +inline Int IABS(Int x) { if (x>=0) return x; return -x;}; + +//----------------------------------------------------------------------------------- +Path::Path(): +m_path(nullptr), +m_pathTail(nullptr), +m_isOptimized(FALSE), +m_blockedByAlly(FALSE), +m_cpopRecentStart(nullptr), +m_cpopCountdown(MAX_CPOP), +m_cpopValid(FALSE) +{ + m_cpopIn.zero(); + m_cpopOut.distAlongPath=0; + m_cpopOut.layer = LAYER_GROUND; + m_cpopOut.posOnPath.zero(); +} + +Path::~Path() +{ + PathNode *node, *nextNode; + + // delete all of the path nodes + for( node = m_path; node; node = nextNode ) + { + nextNode = node->getNext(); + deleteInstance(node); + } +} + +// ------------------------------------------------------------------------------------------------ +/** CRC */ +// ------------------------------------------------------------------------------------------------ +void Path::crc( Xfer *xfer ) +{ +} + +// ------------------------------------------------------------------------------------------------ +/** Xfer Method */ +// ------------------------------------------------------------------------------------------------ +void Path::xfer( Xfer *xfer ) +{ + // version + XferVersion currentVersion = 1; + XferVersion version = currentVersion; + xfer->xferVersion( &version, currentVersion ); + + PathNode *node = m_path; + Int count = 0; + while (node) { + count++; + node = node->getNext(); + } + xfer->xferInt(&count); + + if (xfer->getXferMode() == XFER_SAVE) { + node = m_pathTail; // Write them out backwards. + while (node) { + node->m_id = count; + xfer->xferInt(&count); + Coord3D pos = *node->getPosition(); + xfer->xferCoord3D(&pos); + PathfindLayerEnum layer = node->getLayer(); + xfer->xferUser(&layer, sizeof(layer)); + Bool canOpt = node->getCanOptimize(); + xfer->xferBool(&canOpt); + Int id = -1; + if (node->getNextOptimized()) { + id = node->getNextOptimized()->m_id; + } + xfer->xferInt(&id); + count--; + node = node->getPrevious(); + } + DEBUG_ASSERTCRASH(count==0, ("Wrong data count")); + } else { + m_cpopValid = FALSE; + while (count) { + Int nodeId; + xfer->xferInt(&nodeId); + DEBUG_ASSERTCRASH(nodeId==count, ("Bad data")); + Coord3D pos; + xfer->xferCoord3D(&pos); + PathfindLayerEnum layer; + xfer->xferUser(&layer, sizeof(layer)); + Bool canOpt; + xfer->xferBool(&canOpt); + Int optID = -1; + xfer->xferInt(&optID); + PathNode *node = newInstance(PathNode); + node->m_id = nodeId; + node->setPosition(&pos); + node->setLayer(layer); + node->setCanOptimize(canOpt); + PathNode *optNode = nullptr; + if (optID > 0) { + optNode = m_path; + while (optNode && optNode->m_id != optID) { + optNode = optNode->getNext(); + } + DEBUG_ASSERTCRASH (optNode && optNode->m_id == optID, ("Could not find optimized link.")); + } + m_path = node->prependToList(m_path); + if (m_pathTail == nullptr) + m_pathTail = node; + if (optNode) { + node->setNextOptimized(optNode); + } + count--; + } + } + + xfer->xferBool(&m_isOptimized); + Int obsolete1 = 0; + xfer->xferInt(&obsolete1); + UnsignedInt obsolete2; + xfer->xferUnsignedInt(&obsolete2); + xfer->xferBool(&m_blockedByAlly); + + +#if defined(RTS_DEBUG) + if (TheGlobalData->m_debugAI == AI_DEBUG_PATHS) + { + extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); + RGBColor color; + color.blue = 0; + color.red = color.green = 1; + Coord3D pos; + addIcon(nullptr, 0, 0, color); // erase feedback. + for( PathNode *node = getFirstNode(); node; node = node->getNext() ) + { + + // create objects to show path - they decay + + pos = *node->getPosition(); + addIcon(&pos, PATHFIND_CELL_SIZE_F*.25f, 200, color); + } + + // show optimized path + for( node = getFirstNode(); node; node = node->getNextOptimized() ) + { + pos = *node->getPosition(); + addIcon(&pos, PATHFIND_CELL_SIZE_F*.8f, 200, color); + } + TheAI->pathfinder()->setDebugPath(this); + } +#endif +} + +// ------------------------------------------------------------------------------------------------ +/** Load post process */ +// ------------------------------------------------------------------------------------------------ +void Path::loadPostProcess() +{ +} + +/** + * Create a new node at the head of the path + */ +void Path::prependNode( const Coord3D *pos, PathfindLayerEnum layer ) +{ + PathNode *node = newInstance(PathNode); + + node->setPosition( pos ); + node->setLayer(layer); + + m_path = node->prependToList( m_path ); + + if (m_pathTail == nullptr) + m_pathTail = node; + + m_isOptimized = false; +} + +/** + * Create a new node at the tail of the path + */ +void Path::appendNode( const Coord3D *pos, PathfindLayerEnum layer ) +{ + if (m_isOptimized && m_pathTail) + { + /* Check for duplicates. */ + if (pos->x == m_pathTail->getPosition()->x && pos->y == m_pathTail->getPosition()->y) { + DEBUG_LOG(("Warning - Path Seg length == 0, ignoring. john a.")); + return; + } + } + PathNode *node = newInstance(PathNode); + + node->setPosition( pos ); + node->setLayer(layer); + + if (!m_path) + { + m_path = node; + m_pathTail = node; + + return; + } + + m_pathTail->append(node); + + if (m_isOptimized) + { + m_pathTail->setNextOptimized(node); + } + + m_pathTail = node; +} +/** + * Create a new node at the tail of the path + */ +void Path::updateLastNode( const Coord3D *pos ) +{ + PathfindLayerEnum layer = TheTerrainLogic->getLayerForDestination(pos); + if (m_pathTail) { + m_pathTail->setPosition(pos); + m_pathTail->setLayer(layer); + } + if (m_isOptimized && m_pathTail) + { + PathNode *node = m_path; + while(node && node->getNextOptimized() != m_pathTail) { + node = node->getNextOptimized(); + } + if (node && node->getNextOptimized() == m_pathTail) { + node->setNextOptimized(m_pathTail); + } + } +} + +/** + * Optimize the path by checking line of sight + */ +void Path::optimize( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, Bool blocked ) +{ + PathNode *node, *anchor; + + // start with first node in the path + anchor = getFirstNode(); + + Bool firstNode = true; + PathfindLayerEnum firstLayer = anchor->getLayer(); + + // backwards. + + // + // For each node in the path, check LOS from last node in path, working forward. + // When a clear LOS is found, keep the resulting straight line segment. + // + while( anchor != getLastNode() ) + { + // find the farthest node in the path that has a clear line-of-sight to this anchor + Bool optimizedSegment = false; + PathfindLayerEnum layer = anchor->getLayer(); + PathfindLayerEnum curLayer = anchor->getLayer(); + Int count = 0; + const Int ALLOWED_STEPS = 3; // we can optimize 3 steps to or from a bridge. Otherwise, we need to insert a point. jba. + for (node = anchor->getNext(); node->getNext(); node=node->getNext()) { + count++; + if (curLayer==LAYER_GROUND) { + if (node->getLayer() != curLayer) { + layer = node->getLayer(); + curLayer = layer; + if (count > ALLOWED_STEPS) break; + } + } else { + if (node->getNext()->getLayer() != curLayer) { + if (count > ALLOWED_STEPS) break; + } + } + curLayer = node->getLayer(); + if (node->getCanOptimize()==false) { + break; + } + } + if (firstNode) { + layer = firstLayer; + firstNode = false; + } + //PathfindLayerEnum curLayer = LAYER_GROUND; + for( ; node != anchor; node = node->getPrevious() ) + { + Bool isPassable = false; + //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); + if (TheAI->pathfinder()->isLinePassable( obj, acceptableSurfaces, layer, *anchor->getPosition(), + *node->getPosition(), blocked, false)) + { + isPassable = true; + } + PathfindCell* cell = TheAI->pathfinder()->getCell( layer, node->getPosition()); + if (cell && cell->getType()==PathfindCell::CELL_CLIFF && !cell->getPinched()) { + isPassable = true; + } + // Horizontal, diagonal, and vertical steps are passable. + if (!isPassable) { + Int dx = node->getPosition()->x - anchor->getPosition()->x; + Int dy = node->getPosition()->y - anchor->getPosition()->y; + Bool mightBePassable = false; + if (IABS(dx)==PATHFIND_CELL_SIZE && IABS(dy)==PATHFIND_CELL_SIZE) { + isPassable = true; + } + PathNode *tmpNode; + if (dx==0) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; + if (dx!=0) mightBePassable = false; + } + } + if (dy==0) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; + if (dy!=0) mightBePassable = false; + } + } + if (dx == dy) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; + dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; + if (dy!=dx) mightBePassable = false; + } + } + if (dx == -dy) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; + dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; + if (dy!=-dx) mightBePassable = false; + } + } + if (mightBePassable) { + isPassable = true; + } + } + if (isPassable) + { + // anchor can directly see this node, make it next in the optimized path + anchor->setNextOptimized( node ); + anchor = node; + optimizedSegment = true; + break; + } + } + + if (optimizedSegment == false) + { + // for some reason, there is no clear LOS between the anchor node and the very next node + anchor->setNextOptimized( anchor->getNext() ); + anchor = anchor->getNext(); + } + } + + // the path has been optimized + m_isOptimized = true; +} + +/** + * Optimize the path by checking line of sight + */ +void Path::optimizeGroundPath( Bool crusher, Int pathDiameter ) +{ + PathNode *node, *anchor; + + // start with first node in the path + anchor = getFirstNode(); + + // + // For each node in the path, check LOS from last node in path, working forward. + // When a clear LOS is found, keep the resulting straight line segment. + // + while( anchor != getLastNode() ) + { + // find the farthest node in the path that has a clear line-of-sight to this anchor + Bool optimizedSegment = false; + PathfindLayerEnum layer = anchor->getLayer(); + PathfindLayerEnum curLayer = anchor->getLayer(); + Int count = 0; + const Int ALLOWED_STEPS = 3; // we can optimize 3 steps to or from a bridge. Otherwise, we need to insert a point. jba. + for (node = anchor->getNext(); node->getNext(); node=node->getNext()) { + count++; + if (curLayer==LAYER_GROUND) { + if (node->getLayer() != curLayer) { + layer = node->getLayer(); + curLayer = layer; + if (count > ALLOWED_STEPS) break; + } + } else { + if (node->getNext()->getLayer() != curLayer) { + if (count > ALLOWED_STEPS) break; + } + } + curLayer = node->getLayer(); + } + + // find the farthest node in the path that has a clear line-of-sight to this anchor + for( ; node != anchor; node = node->getPrevious() ) + { + Bool isPassable = false; + //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); + if (TheAI->pathfinder()->isGroundPathPassable( crusher, *anchor->getPosition(), layer, + *node->getPosition(), pathDiameter)) + { + isPassable = true; + } + // Horizontal, diagonal, and vertical steps are passable. + if (!isPassable) { + Int dx = node->getPosition()->x - anchor->getPosition()->x; + Int dy = node->getPosition()->y - anchor->getPosition()->y; + Bool mightBePassable = false; + PathNode *tmpNode; + if (dx==0) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; + if (dx!=0) mightBePassable = false; + } + } + if (dy==0) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; + if (dy!=0) mightBePassable = false; + } + } + if (dx == dy) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; + dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; + if (dy!=dx) mightBePassable = false; + } + } + if (dx == -dy) { + mightBePassable = true; + for (tmpNode = node->getPrevious(); tmpNode && tmpNode != anchor; tmpNode = tmpNode->getPrevious()) { + dx = tmpNode->getNext()->getPosition()->x - tmpNode->getPosition()->x; + dy = tmpNode->getNext()->getPosition()->y - tmpNode->getPosition()->y; + if (dy!=-dx) mightBePassable = false; + } + } + if (mightBePassable) { + isPassable = true; + } + } + if (isPassable) + { + // anchor can directly see this node, make it next in the optimized path + anchor->setNextOptimized( node ); + anchor = node; + optimizedSegment = true; + break; + } + } + + if (optimizedSegment == false) + { + // for some reason, there is no clear LOS between the anchor node and the very next node + anchor->setNextOptimized( anchor->getNext() ); + anchor = anchor->getNext(); + } + } + + // Remove jig/jogs :) jba. + for (anchor=getFirstNode(); anchor!=nullptr; anchor=anchor->getNextOptimized()) { + node = anchor->getNextOptimized(); + if (node && node->getNextOptimized()) { + Real dx = node->getPosition()->x - anchor->getPosition()->x; + Real dy = node->getPosition()->y - anchor->getPosition()->y; + // If the x & y offsets are less than 2 pathfind cells, kill it. + if (dx*dx+dy*dy < sqr(PATHFIND_CELL_SIZE_F)*3.9f) { + anchor->setNextOptimized(node->getNextOptimized()); + } + } + } + + // the path has been optimized + m_isOptimized = true; +} + +inline Bool isReallyClose(const Coord3D& a, const Coord3D& b) +{ + const Real CLOSE_ENOUGH = 0.1f; + return + fabs(a.x-b.x) <= CLOSE_ENOUGH && + fabs(a.y-b.y) <= CLOSE_ENOUGH && + fabs(a.z-b.z) <= CLOSE_ENOUGH; +} + +/** + * Given a location, return the closest position on the path. + * If 'allowBacktrack' is true, the entire path is considered. + * If it is false, the point computed cannot be prior to previously returned non-backtracking points on this path. + * Because the path "knows" the direction of travel, it will "lead" the given position a bit + * to ensure the path is followed in the intended direction. + * + * Note: The path cleanup does not take into account rolling terrain, so we can end up with + * these situations: + * + * B + * ###### + * ########## + * A-##----------##---C + * ####################### + * + * + * When an agent gets to B, he seems far off of the path, but it really not. + * There are similar problems with valleys. + * + * Since agents track the closest path, if a high hill gets close to the underside of + * a bridge, an agent may 'jump' to the higher path. This must be avoided in maps. + * + * return along-path distance to the end will be returned as function result + */ +void Path::computePointOnPath( + const Object* obj, + const LocomotorSet& locomotorSet, + const Coord3D& pos, + ClosestPointOnPathInfo& out +) +{ + CRCDEBUG_LOG(("Path::computePointOnPath() for %s", DebugDescribeObject(obj).str())); + + out.layer = LAYER_GROUND; + out.posOnPath.zero(); + out.distAlongPath = 0; + + if (m_path == nullptr) + { + m_cpopValid = false; + return; + } + out.layer = m_path->getLayer(); + + if (m_cpopValid && m_cpopCountdown>0 && isReallyClose(pos, m_cpopIn)) + { + out = m_cpopOut; + m_cpopCountdown--; + CRCDEBUG_LOG(("Path::computePointOnPath() end because we're really close")); + return; + } + m_cpopCountdown = MAX_CPOP; + + // default pathPos to end of the path + out.posOnPath = *getLastNode()->getPosition(); + + const PathNode* closeNode = nullptr; + Coord2D toPos; + Real closeDistSqr = 99999999.9f; + Real totalPathLength = 0.0f; + Real lengthAlongPathToPos = 0.0f; + + // + // Find the closest segment of the path + // + const PathNode* prevNode = m_path; + Coord2D segmentDirNorm; + Real segmentLength; + + // note that the seg dir and len returned by this is the dist & vec from 'prevNode' to 'node' + for ( const PathNode* node = prevNode->getNextOptimized(&segmentDirNorm, &segmentLength); + node != nullptr; + node = node->getNextOptimized(&segmentDirNorm, &segmentLength) ) + { + const Coord3D* prevNodePos = prevNode->getPosition(); + const Coord3D* nodePos = node->getPosition(); + + // compute vector from start of segment to pos + toPos.x = pos.x - prevNodePos->x; + toPos.y = pos.y - prevNodePos->y; + + // compute distance projection of 'toPos' onto segment + Real alongPathDist = segmentDirNorm.x * toPos.x + segmentDirNorm.y * toPos.y; + + Coord3D pointOnPath; + if (alongPathDist < 0.0f) + { + // projected point is before start of segment, use starting point + alongPathDist = 0.0f; + pointOnPath = *prevNodePos; + } + else if (alongPathDist > segmentLength) + { + // projected point is beyond end of segment, use end point + if (node->getNextOptimized() == nullptr) + { + alongPathDist = segmentLength; + pointOnPath = *nodePos; + } + else + { + // beyond the end of this segment, skip this segment + // if bend is sharp, start of next segment will grab this point + // if bend is gradual, the point will project into the next segment + totalPathLength += segmentLength; + prevNode = node; + continue; + } + } + else + { + // projected point is on this segment, compute it + pointOnPath.x = prevNodePos->x + alongPathDist * segmentDirNorm.x; + pointOnPath.y = prevNodePos->y + alongPathDist * segmentDirNorm.y; + pointOnPath.z = 0; + } + + // compute distance to point on path, and track the closest we've found so far + Coord2D offset; + offset.x = pos.x - pointOnPath.x; + offset.y = pos.y - pointOnPath.y; + + Real offsetDistSqr = offset.x*offset.x + offset.y*offset.y; + if (offsetDistSqr < closeDistSqr) + { + closeDistSqr = offsetDistSqr; + closeNode = prevNode; + out.posOnPath = pointOnPath; + + lengthAlongPathToPos = totalPathLength + alongPathDist; + } + + // add this segment's length to find total path length + /// @todo Precompute this and store in path + totalPathLength += segmentLength; + prevNode = node; + DUMPCOORD3D(&pointOnPath); + } + + // + // Compute the goal movement position for this agent + // + if (closeNode && closeNode->getNextOptimized()) + { + // note that the seg dir and len returned by this is the dist & vec from 'closeNode' to 'closeNext' + const PathNode* closeNext = closeNode->getNextOptimized(&segmentDirNorm, &segmentLength); + const Coord3D* nextNodePos = closeNext->getPosition(); + const Coord3D* closeNodePos = closeNode->getPosition(); + + const PathNode* closePrev = closeNode->getPrevious(); + if (closePrev && closePrev->getLayer() > LAYER_GROUND) + { + out.layer = closeNode->getLayer(); + } + if (closeNode->getLayer() > LAYER_GROUND) + { + out.layer = closeNode->getLayer(); + } + + if (closeNext->getLayer() > LAYER_GROUND) + { + out.layer = closeNext->getLayer(); + } + + // compute vector from start of segment to pos + toPos.x = pos.x - closeNodePos->x; + toPos.y = pos.y - closeNodePos->y; + + // compute distance projection of 'toPos' onto segment + Real alongPathDist = segmentDirNorm.x * toPos.x + segmentDirNorm.y * toPos.y; + + // we know this is the closest segment, so don't allow farther back than the start node + if (alongPathDist < 0.0f) + alongPathDist = 0.0f; + + // compute distance of point from this path segment + Real toDistSqr = sqr(toPos.x) + sqr(toPos.y); + Real offsetDistSq = toDistSqr - sqr(alongPathDist); + Real offsetDist = (offsetDistSq <= 0.0) ? 0.0 : sqrt(offsetDistSq); + + // If we are basically on the path, return the next path node as the movement goal. + // However, the farther off the path we get, the movement goal becomes closer to our + // projected position on the path. If we are very far off the path, we will move + // directly towards the nearest point on the path, and not the next path node. + const Real maxPathError = 3.0f * PATHFIND_CELL_SIZE_F; + const Real maxPathErrorInv = 1.0 / maxPathError; + Real k = offsetDist * maxPathErrorInv; + if (k > 1.0f) + k = 1.0f; + + Bool gotPos = false; + CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 1")); + if (TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), out.layer, pos, *nextNodePos, + false, true )) + { + out.posOnPath = *nextNodePos; + gotPos = true; + + Bool tryAhead = alongPathDist > segmentLength * 0.5; + if (closeNext->getCanOptimize() == false) + { + tryAhead = false; // don't go past no-opt nodes. + } + if (closeNode->getLayer() != closeNext->getLayer()) + { + tryAhead = false; // don't go past layers. + } + if (obj->getLayer()!=LAYER_GROUND) { + tryAhead = false; + } + Bool veryClose = false; + if (segmentLength-alongPathDist<1.0f) { + tryAhead = true; + veryClose = true; + } + if (tryAhead) + { + // try next segment middle. + const PathNode *next = closeNext->getNextOptimized(); + if (next) + { + Coord3D tryPos; + tryPos.x = (nextNodePos->x + next->getPosition()->x) * 0.5; + tryPos.y = (nextNodePos->y + next->getPosition()->y) * 0.5; + tryPos.z = nextNodePos->z; + CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 2")); + if (veryClose || TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), closeNext->getLayer(), pos, tryPos, false, true )) + { + gotPos = true; + out.posOnPath = tryPos; + } + } + } + } + else if (k > 0.5f) + { + Real tryDist = alongPathDist + (0.5) * (segmentLength - alongPathDist); + + // projected point is on this segment, compute it + out.posOnPath.x = closeNodePos->x + tryDist * segmentDirNorm.x; + out.posOnPath.y = closeNodePos->y + tryDist * segmentDirNorm.y; + out.posOnPath.z = closeNodePos->z; + + CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 3")); + if (TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), out.layer, pos, out.posOnPath, false, true )) + { + k = 0.5f; + gotPos = true; + } + } + + // if we are on the path (k == 0), then alongPathDist == segmentLength + // if we are way off the path (k == 1), then alongPathDist is unchanged, and it projection of actual pos + alongPathDist += (1.0f - k) * (segmentLength - alongPathDist); + + if (!gotPos) + { + if (alongPathDist > segmentLength) + { + alongPathDist = segmentLength; + out.posOnPath = *nextNodePos; + } + else + { + // projected point is on this segment, compute it + out.posOnPath.x = closeNodePos->x + alongPathDist * segmentDirNorm.x; + out.posOnPath.y = closeNodePos->y + alongPathDist * segmentDirNorm.y; + out.posOnPath.z = closeNodePos->z; + Real dx = fabs(pos.x - out.posOnPath.x); + Real dy = fabs(pos.y - out.posOnPath.y); + if (dx<1 && dy<1 && closeNode->getNextOptimized() && closeNode->getNextOptimized()->getNextOptimized()) { + out.posOnPath = *closeNode->getNextOptimized()->getNextOptimized()->getPosition(); + } + } + } + } + + TheAI->pathfinder()->setDebugPathPosition( &out.posOnPath ); + + out.distAlongPath = totalPathLength - lengthAlongPathToPos; + + Coord3D delta; + delta.x = out.posOnPath.x - pos.x; + delta.y = out.posOnPath.y - pos.y; + delta.z = 0; + Real lenDelta = delta.length(); + if (lenDelta > out.distAlongPath && out.distAlongPath > PATHFIND_CLOSE_ENOUGH) + { + out.distAlongPath = lenDelta; + } + + m_cpopIn = pos; + m_cpopOut = out; + m_cpopValid = true; + CRCDEBUG_LOG(("Path::computePointOnPath() end")); + +} + + +/** + Given a position, computes the distance to the goal. Returns 0 if we are past the goal. + Returns the goal position in goalPos. This is intended for use with flying paths, that go + directly to the goal and don't consider obstacles. jba. + */ +Real Path::computeFlightDistToGoal( const Coord3D *pos, Coord3D& goalPos ) +{ + if (m_path == nullptr) + { + goalPos.x = 0.0f; + goalPos.y = 0.0f; + goalPos.z = 0.0f; + return 0.0f; + } + const PathNode *curNode = getFirstNode(); + if (m_cpopRecentStart) { + curNode = m_cpopRecentStart; + } else { + m_cpopRecentStart = curNode; + } + const PathNode *nextNode = curNode->getNextOptimized(); + goalPos = *curNode->getPosition(); + Real distance = 0; + Bool useNext = true; + while (nextNode) { + + if (useNext) { + goalPos = *nextNode->getPosition(); + } + + Coord3D startPos = *curNode->getPosition(); + Coord3D endPos = *nextNode->getPosition(); + + Coord2D posToGoalVector; + // posToGoalVector is pos to goalPos vector. + posToGoalVector.x = endPos.x - pos->x; + posToGoalVector.y = endPos.y - pos->y; + + // pathVector is the startPos to goal pos vector. + Coord2D pathVector; + pathVector.x = endPos.x - startPos.x; + pathVector.y = endPos.y - startPos.y; + + // Normalize pathVector + pathVector.normalize(); + + // Dot product is the posToGoal vector projected onto the path vector. + Real dotProduct = posToGoalVector.x*pathVector.x + posToGoalVector.y*pathVector.y; + if (dotProduct>=0) { + distance += dotProduct; + useNext = false; + } else if (useNext) { + m_cpopRecentStart = nextNode; + } + curNode = nextNode; + nextNode = curNode->getNextOptimized(); + } + return distance; + +} From fcad3253cb393850814233d95fc6dd6d5cdd30af Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:21:12 +0200 Subject: [PATCH 03/10] refactor(pathfinder): Move PathfindCellInfo implementation to its own file (#3429) --- Core/GameEngine/CMakeLists.txt | 1 + .../Source/GameLogic/AI/AIPathfind.cpp | 96 --------------- .../AI/Pathfinder/PathfindCellInfo.cpp | 116 ++++++++++++++++++ 3 files changed, 117 insertions(+), 96 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index b69e48170d2..8762acd909c 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -863,6 +863,7 @@ set(GAMEENGINE_SRC # Source/GameLogic/AI/AIStates.cpp # Source/GameLogic/AI/AITNGuard.cpp Source/GameLogic/AI/Pathfinder/Path.cpp + Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp # Source/GameLogic/AI/Squad.cpp # Source/GameLogic/AI/TurretAI.cpp diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index d463053cbac..49a6b2bb508 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -117,12 +117,6 @@ constexpr const UnsignedInt MAX_ADJUSTMENT_CELL_COUNT = 400; constexpr const UnsignedInt MAX_SAFE_PATH_CELL_COUNT = 2000; constexpr const UnsignedInt PATHFIND_CELLS_PER_FRAME = 5000; // Number of cells we will search pathfinding per frame. -constexpr const UnsignedInt CELL_INFOS_TO_ALLOCATE = 30000; - -//----------------------------------------------------------------------------------- - -PathfindCellInfo *PathfindCellInfo::s_infoArray = nullptr; -PathfindCellInfo *PathfindCellInfo::s_firstFree = nullptr; #if RETAIL_COMPATIBLE_PATHFINDING // TheSuperHackers @info This variable is here so the code will run down the retail compatible path till a failure mode is hit @@ -130,16 +124,6 @@ PathfindCellInfo *PathfindCellInfo::s_firstFree = nullptr; Bool s_useFixedPathfinding = false; Bool s_forceCleanCells = false; -void PathfindCellInfo::forceCleanPathFindCellInfos() -{ - for (Int i = 0; i < CELL_INFOS_TO_ALLOCATE - 1; i++) { - s_infoArray[i].m_nextOpen = nullptr; - s_infoArray[i].m_prevOpen = nullptr; - s_infoArray[i].m_open = FALSE; - s_infoArray[i].m_closed = FALSE; - } -} - void Pathfinder::forceCleanCells() { UnicodeString pathfinderFailoverMessage = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:PathfindingCrashPrevented", L"A pathfinding crash was prevented at frame %u, now switching to the crash fixed pathfinding.", TheGameLogic->getFrame()); @@ -172,86 +156,6 @@ void Pathfinder::forceCleanCells() } #endif -/** - * Allocates a pool of pathfind cell infos. - */ -void PathfindCellInfo::allocateCellInfos() -{ - releaseCellInfos(); - s_infoArray = MSGNEW("PathfindCellInfo") PathfindCellInfo[CELL_INFOS_TO_ALLOCATE]; // pool[]ify - s_infoArray[CELL_INFOS_TO_ALLOCATE-1].m_pathParent = nullptr; - s_infoArray[CELL_INFOS_TO_ALLOCATE-1].m_isFree = true; - s_firstFree = s_infoArray; - for (Int i=0; im_isFree, ("Should be freed.")); - s_firstFree = s_firstFree->m_pathParent; - } - DEBUG_ASSERTCRASH(count==CELL_INFOS_TO_ALLOCATE, ("Error - Allocated cellinfos.")); - delete[] s_infoArray; - s_infoArray = nullptr; - s_firstFree = nullptr; -} - -/** - * Gets a pathfindcellinfo. - */ -PathfindCellInfo *PathfindCellInfo::getACellInfo(PathfindCell *cell,const ICoord2D &pos) -{ - PathfindCellInfo *info = s_firstFree; - if (s_firstFree) { - DEBUG_ASSERTCRASH(s_firstFree->m_isFree, ("Should be freed.")); - s_firstFree = s_firstFree->m_pathParent; - info->m_isFree = false; // Just allocated it. - info->m_cell = cell; - info->m_pos = pos; - - info->m_nextOpen = nullptr; - info->m_prevOpen = nullptr; - info->m_pathParent = nullptr; - info->m_costSoFar = 0; - info->m_totalCost = 0; - info->m_open = 0; - info->m_closed = 0; - info->m_obstacleID = INVALID_ID; - info->m_goalUnitID = INVALID_ID; - info->m_posUnitID = INVALID_ID; - info->m_goalAircraftID = INVALID_ID; - info->m_obstacleIsFence = false; - info->m_obstacleIsTransparent = false; - info->m_blockedByAlly = false; - } - return info; -} - -/** - * Returns a pathfindcellinfo. - */ -void PathfindCellInfo::releaseACellInfo(PathfindCellInfo *theInfo) -{ - DEBUG_ASSERTCRASH(!theInfo->m_isFree, ("Shouldn't be free.")); - //@ todo -fix this assert on usa04. jba. - //DEBUG_ASSERTCRASH(theInfo->m_obstacleID==0, ("Shouldn't be obstacle.")); - theInfo->m_pathParent = s_firstFree; - s_firstFree = theInfo; - s_firstFree->m_isFree = true; -} - //----------------------------------------------------------------------------------- Bool PathfindCellList::canReverseSort(PathfindCell& currentCell) const diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp new file mode 100644 index 00000000000..c1541f8fcc1 --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp @@ -0,0 +1,116 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/Pathfinder/PathfindCellInfo.h" + +constexpr const UnsignedInt CELL_INFOS_TO_ALLOCATE = 30000; + +PathfindCellInfo *PathfindCellInfo::s_infoArray = nullptr; +PathfindCellInfo *PathfindCellInfo::s_firstFree = nullptr; + +#if RETAIL_COMPATIBLE_PATHFINDING +void PathfindCellInfo::forceCleanPathFindCellInfos() +{ + for (Int i = 0; i < CELL_INFOS_TO_ALLOCATE - 1; i++) { + s_infoArray[i].m_nextOpen = nullptr; + s_infoArray[i].m_prevOpen = nullptr; + s_infoArray[i].m_open = FALSE; + s_infoArray[i].m_closed = FALSE; + } +} +#endif + +/** + * Allocates a pool of pathfind cell infos. + */ +void PathfindCellInfo::allocateCellInfos() +{ + releaseCellInfos(); + s_infoArray = MSGNEW("PathfindCellInfo") PathfindCellInfo[CELL_INFOS_TO_ALLOCATE]; // pool[]ify + s_infoArray[CELL_INFOS_TO_ALLOCATE-1].m_pathParent = nullptr; + s_infoArray[CELL_INFOS_TO_ALLOCATE-1].m_isFree = true; + s_firstFree = s_infoArray; + for (Int i=0; im_isFree, ("Should be freed.")); + s_firstFree = s_firstFree->m_pathParent; + } + DEBUG_ASSERTCRASH(count==CELL_INFOS_TO_ALLOCATE, ("Error - Allocated cellinfos.")); + delete[] s_infoArray; + s_infoArray = nullptr; + s_firstFree = nullptr; +} + +/** + * Gets a pathfindcellinfo. + */ +PathfindCellInfo *PathfindCellInfo::getACellInfo(PathfindCell *cell,const ICoord2D &pos) +{ + PathfindCellInfo *info = s_firstFree; + if (s_firstFree) { + DEBUG_ASSERTCRASH(s_firstFree->m_isFree, ("Should be freed.")); + s_firstFree = s_firstFree->m_pathParent; + info->m_isFree = false; // Just allocated it. + info->m_cell = cell; + info->m_pos = pos; + + info->m_nextOpen = nullptr; + info->m_prevOpen = nullptr; + info->m_pathParent = nullptr; + info->m_costSoFar = 0; + info->m_totalCost = 0; + info->m_open = 0; + info->m_closed = 0; + info->m_obstacleID = INVALID_ID; + info->m_goalUnitID = INVALID_ID; + info->m_posUnitID = INVALID_ID; + info->m_goalAircraftID = INVALID_ID; + info->m_obstacleIsFence = false; + info->m_obstacleIsTransparent = false; + info->m_blockedByAlly = false; + } + return info; +} + +/** + * Returns a pathfindcellinfo. + */ +void PathfindCellInfo::releaseACellInfo(PathfindCellInfo *theInfo) +{ + DEBUG_ASSERTCRASH(!theInfo->m_isFree, ("Shouldn't be free.")); + //@ todo -fix this assert on usa04. jba. + //DEBUG_ASSERTCRASH(theInfo->m_obstacleID==0, ("Shouldn't be obstacle.")); + theInfo->m_pathParent = s_firstFree; + s_firstFree = theInfo; + s_firstFree->m_isFree = true; +} From 999977639f41c5f0022b17d9b9ca4c26cac6fb94 Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:54:08 +0200 Subject: [PATCH 04/10] refactor(pathfinder): Move shared constants to their own file (#3429) --- Core/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/GameLogic/AIPathfind.h | 10 +--- .../GameLogic/Pathfinder/PathfindConstants.h | 48 +++++++++++++++++++ .../Source/GameLogic/AI/AIPathfind.cpp | 15 +----- .../AI/Pathfinder/PathfindConstants.cpp | 24 ++++++++++ 5 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 Core/GameEngine/Include/GameLogic/Pathfinder/PathfindConstants.h create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 8762acd909c..1304591e088 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -491,6 +491,7 @@ set(GAMEENGINE_SRC Include/GameLogic/Pathfinder/PathfindCell.h Include/GameLogic/Pathfinder/PathfindCellInfo.h Include/GameLogic/Pathfinder/PathfindCellList.h + Include/GameLogic/Pathfinder/PathfindConstants.h Include/GameLogic/Pathfinder/PathfindLayer.h Include/GameLogic/Pathfinder/PathfindZoneManager.h Include/GameLogic/Pathfinder/PathNode.h @@ -864,6 +865,7 @@ set(GAMEENGINE_SRC # Source/GameLogic/AI/AITNGuard.cpp Source/GameLogic/AI/Pathfinder/Path.cpp Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp + Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp # Source/GameLogic/AI/Squad.cpp # Source/GameLogic/AI/TurretAI.cpp diff --git a/Core/GameEngine/Include/GameLogic/AIPathfind.h b/Core/GameEngine/Include/GameLogic/AIPathfind.h index 1486394985c..e654942cd32 100644 --- a/Core/GameEngine/Include/GameLogic/AIPathfind.h +++ b/Core/GameEngine/Include/GameLogic/AIPathfind.h @@ -37,6 +37,7 @@ #include "Pathfinder/PathfindCell.h" #include "Pathfinder/PathfindCellInfo.h" #include "Pathfinder/PathfindCellList.h" +#include "Pathfinder/PathfindConstants.h" #include "Pathfinder/PathfindLayer.h" #include "Pathfinder/PathfindZoneManager.h" #include "Pathfinder/PathNode.h" @@ -67,16 +68,7 @@ class PathfindCell; // See GameType.h for // enum {LAYER_INVALID = 0, LAYER_GROUND = 1, LAYER_TOP=2 }; -// Fits in 4 bits for now -enum {MAX_WALL_PIECES = 128}; -// how close a unit has to be in z to interact with the layer. -#define LAYER_Z_CLOSE_ENOUGH_F 10.0f - -#define PATHFIND_CELL_SIZE 10 -#define PATHFIND_CELL_SIZE_F 10.0f - -enum { PATHFIND_QUEUE_LEN=512}; struct TCheckMovementInfo; diff --git a/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindConstants.h b/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindConstants.h new file mode 100644 index 00000000000..78bddbc9d59 --- /dev/null +++ b/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindConstants.h @@ -0,0 +1,48 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#pragma once + +constexpr const Int MAX_WALL_PIECES = 128; +constexpr const Int PATHFIND_QUEUE_LEN = 512; + +// how close a unit has to be in z to interact with the layer. +constexpr const Real LAYER_Z_CLOSE_ENOUGH_F = 10.0f; + +constexpr const UnsignedInt PATHFIND_CELL_SIZE = 10; +constexpr const Real PATHFIND_CELL_SIZE_F = 10.0f; + +constexpr const UnsignedInt ZONE_UPDATE_FREQUENCY = 300; +constexpr const UnsignedInt MAX_CELL_COUNT = 500; +constexpr const UnsignedInt MAX_ADJUSTMENT_CELL_COUNT = 400; +constexpr const UnsignedInt MAX_SAFE_PATH_CELL_COUNT = 2000; + +// Number of cells we will search pathfinding per frame. +constexpr const UnsignedInt PATHFIND_CELLS_PER_FRAME = 5000; + +constexpr const Int COST_ORTHOGONAL = 10; +constexpr const Int COST_DIAGONAL = 14; +constexpr const Real COST_TO_DISTANCE_FACTOR = 1.0f / 10.0f; +constexpr const Real COST_TO_DISTANCE_FACTOR_SQR = COST_TO_DISTANCE_FACTOR * COST_TO_DISTANCE_FACTOR; + +#if RETAIL_COMPATIBLE_PATHFINDING +// TheSuperHackers @info This variable is here so the code will run down the retail compatible path till a failure mode is hit +// The pathfinding will then switch over to the corrected pathfinding code for SH clients +extern Bool s_useFixedPathfinding; +extern Bool s_forceCleanCells; +#endif diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 49a6b2bb508..8c1ba39f82e 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -28,6 +28,7 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "GameLogic/AIPathfind.h" +#include "GameLogic/Pathfinder/PathfindConstants.h" #include "Common/PerfTimer.h" #include "Common/Player.h" @@ -111,18 +112,8 @@ inline Int IABS(Int x) { if (x>=0) return x; return -x;}; //----------------------------------------------------------------------------------- static Int frameToShowObstacles; -constexpr const UnsignedInt ZONE_UPDATE_FREQUENCY = 300; -constexpr const UnsignedInt MAX_CELL_COUNT = 500; -constexpr const UnsignedInt MAX_ADJUSTMENT_CELL_COUNT = 400; -constexpr const UnsignedInt MAX_SAFE_PATH_CELL_COUNT = 2000; - -constexpr const UnsignedInt PATHFIND_CELLS_PER_FRAME = 5000; // Number of cells we will search pathfinding per frame. #if RETAIL_COMPATIBLE_PATHFINDING -// TheSuperHackers @info This variable is here so the code will run down the retail compatible path till a failure mode is hit -// The pathfinding will then switch over to the corrected pathfinding code for SH clients -Bool s_useFixedPathfinding = false; -Bool s_forceCleanCells = false; void Pathfinder::forceCleanCells() { @@ -1000,10 +991,6 @@ inline Bool PathfindCell::isObstacleFence() const } -const Int COST_ORTHOGONAL = 10; -const Int COST_DIAGONAL = 14; -const Real COST_TO_DISTANCE_FACTOR = 1.0f/10.0f; -const Real COST_TO_DISTANCE_FACTOR_SQR = COST_TO_DISTANCE_FACTOR*COST_TO_DISTANCE_FACTOR; UnsignedInt PathfindCell::costToGoal( PathfindCell *goal ) { diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp new file mode 100644 index 00000000000..b9b95d9b30e --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp @@ -0,0 +1,24 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/Pathfinder/PathfindConstants.h" + +#if RETAIL_COMPATIBLE_PATHFINDING +Bool s_useFixedPathfinding = false; +Bool s_forceCleanCells = false; +#endif From dee1d77160727ace13ae533192c5347a1114c3be Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:11:22 +0200 Subject: [PATCH 05/10] refactor(pathfinder): Move PathfindCell to its own file (#3429) Some inlined functions had to be made non-inline for this to compile. --- Core/GameEngine/CMakeLists.txt | 1 + .../GameLogic/Pathfinder/PathfindCell.h | 40 +- .../Source/GameLogic/AI/AIPathfind.cpp | 935 ----------------- .../GameLogic/AI/Pathfinder/PathfindCell.cpp | 954 ++++++++++++++++++ 4 files changed, 975 insertions(+), 955 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 1304591e088..1e193f26ddd 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -864,6 +864,7 @@ set(GAMEENGINE_SRC # Source/GameLogic/AI/AIStates.cpp # Source/GameLogic/AI/AITNGuard.cpp Source/GameLogic/AI/Pathfinder/Path.cpp + Source/GameLogic/AI/Pathfinder/PathfindCell.cpp Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp diff --git a/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h b/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h index ac879d5da34..a675ab807a9 100644 --- a/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h +++ b/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h @@ -79,8 +79,8 @@ class PathfindCell void clearObstruction() { m_type = CELL_CLEAR; m_obstacleID = INVALID_ID; m_obstacleIsFence = false; m_obstacleIsTransparent = false; } #endif - inline Bool isObstacleTransparent() const; - inline Bool isObstacleFence() const; + Bool isObstacleTransparent() const; + Bool isObstacleFence() const; /// Return estimated cost from given cell to reach goal cell UnsignedInt costToGoal( PathfindCell *goal ); @@ -118,29 +118,29 @@ class PathfindCell /// remove all cells from closed list. static Int releaseOpenList( PathfindCellList &list ); - inline PathfindCell *getNextOpen() {return m_info->m_nextOpen?m_info->m_nextOpen->m_cell: nullptr;} - inline PathfindCell *getPrevOpen() {return m_info->m_prevOpen?m_info->m_prevOpen->m_cell: nullptr;} + PathfindCell *getNextOpen() {return m_info->m_nextOpen?m_info->m_nextOpen->m_cell: nullptr;} + PathfindCell *getPrevOpen() {return m_info->m_prevOpen?m_info->m_prevOpen->m_cell: nullptr;} - inline UnsignedShort getXIndex() const {return m_info->m_pos.x;} - inline UnsignedShort getYIndex() const {return m_info->m_pos.y;} + UnsignedShort getXIndex() const {return m_info->m_pos.x;} + UnsignedShort getYIndex() const {return m_info->m_pos.y;} - inline Bool isBlockedByAlly() const; - inline void setBlockedByAlly(Bool blocked); + Bool isBlockedByAlly() const; + void setBlockedByAlly(Bool blocked); - inline Bool getOpen() const {return m_info->m_open;} - inline Bool getClosed() const {return m_info->m_closed;} - inline UnsignedInt getCostSoFar() const {return m_info->m_costSoFar;} - inline UnsignedInt getTotalCost() const {return m_info->m_totalCost;} + Bool getOpen() const {return m_info->m_open;} + Bool getClosed() const {return m_info->m_closed;} + UnsignedInt getCostSoFar() const {return m_info->m_costSoFar;} + UnsignedInt getTotalCost() const {return m_info->m_totalCost;} - inline UnsignedInt getTotalCostDifference(PathfindCell& other) const; + UnsignedInt getTotalCostDifference(PathfindCell& other) const; - inline void setCostSoFar(UnsignedInt cost) { if( m_info ) m_info->m_costSoFar = cost;} - inline void setTotalCost(UnsignedInt cost) { if( m_info ) m_info->m_totalCost = cost;} + void setCostSoFar(UnsignedInt cost) { if( m_info ) m_info->m_costSoFar = cost;} + void setTotalCost(UnsignedInt cost) { if( m_info ) m_info->m_totalCost = cost;} void setParentCell(PathfindCell* parent); void clearParentCell(); void setParentCellHierarchical(PathfindCell* parent); - inline PathfindCell* getParentCell() const {return m_info ? m_info->m_pathParent ? m_info->m_pathParent->m_cell : nullptr : nullptr;} + PathfindCell* getParentCell() const {return m_info ? m_info->m_pathParent ? m_info->m_pathParent->m_cell : nullptr : nullptr;} Bool startPathfind( PathfindCell *goalCell ); Bool getPinched() const {return m_pinched;} @@ -154,11 +154,11 @@ class PathfindCell void setGoalUnit(ObjectID unit, const ICoord2D &pos ); void setGoalAircraft(ObjectID unit, const ICoord2D &pos ); void setPosUnit(ObjectID unit, const ICoord2D &pos ); - inline ObjectID getGoalUnit() const {ObjectID id = m_info?m_info->m_goalUnitID:INVALID_ID; return id;} - inline ObjectID getGoalAircraft() const {ObjectID id = m_info?m_info->m_goalAircraftID:INVALID_ID; return id;} - inline ObjectID getPosUnit() const {ObjectID id = m_info?m_info->m_posUnitID:INVALID_ID; return id;} + ObjectID getGoalUnit() const {ObjectID id = m_info?m_info->m_goalUnitID:INVALID_ID; return id;} + ObjectID getGoalAircraft() const {ObjectID id = m_info?m_info->m_goalAircraftID:INVALID_ID; return id;} + ObjectID getPosUnit() const {ObjectID id = m_info?m_info->m_posUnitID:INVALID_ID; return id;} - inline ObjectID getObstacleID() const; + ObjectID getObstacleID() const; void setLayer( PathfindLayerEnum layer ) { m_layer = layer; } ///< set the cell layer PathfindLayerEnum getLayer() const { return (PathfindLayerEnum)m_layer; } ///< get the cell layer diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 8c1ba39f82e..8f2d572c3c5 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -157,941 +157,6 @@ Bool PathfindCellList::canReverseSort(PathfindCell& currentCell) const return false; } -//----------------------------------------------------------------------------------- - -/** - * Constructor - */ -PathfindCell::PathfindCell() :m_info(nullptr) -{ - reset(); -} - -/** - * Destructor - */ -PathfindCell::~PathfindCell() -{ - if (m_info) PathfindCellInfo::releaseACellInfo(m_info); - m_info = nullptr; - static Bool warn = true; - if (warn) { - warn = false; - DEBUG_LOG( ("PathfindCell::~PathfindCell m_info Allocated.")); - } -} - -/** - * Reset the cell to default values - */ -void PathfindCell::reset() -{ - m_type = PathfindCell::CELL_CLEAR; - m_flags = PathfindCell::NO_UNITS; - m_zone = 0; - m_aircraftGoal = false; - m_pinched = false; - if (m_info) { - m_info->m_obstacleID = INVALID_ID; - PathfindCellInfo::releaseACellInfo(m_info); - m_info = nullptr; - } - m_obstacleID = INVALID_ID; - m_blockedByAlly = false; - m_obstacleIsFence = false; - m_obstacleIsTransparent = false; - - m_connectsToLayer = LAYER_INVALID; - m_layer = LAYER_GROUND; - -} - -/** - * Reset the pathfinding values in the cell. - */ -Bool PathfindCell::startPathfind( PathfindCell *goalCell ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - m_info->m_nextOpen = nullptr; - m_info->m_prevOpen = nullptr; - m_info->m_pathParent = nullptr; - m_info->m_costSoFar = 0; // start node, no cost to get here - m_info->m_totalCost = 0; - if (goalCell) { - m_info->m_totalCost = costToGoal( goalCell ); - } -#if RETAIL_COMPATIBLE_PATHFINDING - if (!s_useFixedPathfinding) { - m_info->m_open = TRUE; - } else -#endif - { - m_info->m_open = FALSE; - } - m_info->m_closed = FALSE; - return true; -} - -/** - * Set the blocked by ally flag on the pathfind cell info. - */ -inline Bool PathfindCell::isBlockedByAlly() const -{ -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - return m_blockedByAlly; - } - - return m_info->m_blockedByAlly; -#else - return m_blockedByAlly; -#endif -} - -inline void PathfindCell::setBlockedByAlly(Bool blocked) -{ -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - m_blockedByAlly = (blocked != 0); - return; - } - - m_info->m_blockedByAlly = (blocked != 0); -#else - m_blockedByAlly = (blocked != 0); -#endif -} - -/** - * Determine absolute total path cost difference between two cells. - * Returns UINT_MAX if used with an uninitialised cell, so will be sorted as maximally dissimilar. - */ -inline UnsignedInt PathfindCell::getTotalCostDifference(PathfindCell& other) const -{ - if (m_info && other.m_info) - return abs((Int)m_info->m_totalCost - (Int)other.m_info->m_totalCost); - - return UINT_MAX; -} - -/** - * Set the parent pointer. - */ -void PathfindCell::setParentCell( PathfindCell* parent ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - m_info->m_pathParent = parent->m_info; - Int dx = m_info->m_pos.x - parent->m_info->m_pos.x; - Int dy = m_info->m_pos.y - parent->m_info->m_pos.y; - if (dx<-1 || dx>1 || dy<-1 || dy>1) { - DEBUG_CRASH(("Invalid parent index.")); - } -} - -/** - * Set the parent pointer. - */ -void PathfindCell::setParentCellHierarchical( PathfindCell* parent ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - m_info->m_pathParent = parent->m_info; -} - -/** - * Reset the parent cell. - */ -void PathfindCell::clearParentCell( ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - m_info->m_pathParent = nullptr; -} - - -/** - * Allocates an info record for a cell. - */ -Bool PathfindCell::allocateInfo( const ICoord2D &pos ) -{ - if (!m_info) { - m_info = PathfindCellInfo::getACellInfo(this, pos); - return (m_info != nullptr); - } - return true; -} - -/** - * Releases an info record for a cell. - */ -void PathfindCell::releaseInfo() -{ - // TheSuperHackers @bugfix Mauller/SkyAero 05/06/2025 Parent cell links need clearing to prevent dangling pointers on starting points that can link them to an invalid parent cell. - // Parent cells are only cleared within Pathfinder::prependCells, so cells that do not make it onto the final path do not get their parent cell cleared. - // Cells with a special flags also do not get their PathfindCellInfo cleared and therefore can leave a parent cell set on a starting cell. -#if RETAIL_COMPATIBLE_PATHFINDING - if (s_useFixedPathfinding) -#endif - { - if (m_info) { - m_info->m_pathParent = nullptr; - } - } - - if (m_type == PathfindCell::CELL_OBSTACLE || m_flags != NO_UNITS || m_aircraftGoal) { - return; - } - - if (!m_info) { - return; - } - - DEBUG_ASSERTCRASH(m_info->m_prevOpen==nullptr && m_info->m_nextOpen==nullptr, ("Shouldn't be linked.")); - DEBUG_ASSERTCRASH(m_info->m_open==0 && m_info->m_closed==0, ("Shouldn't be linked.")); - DEBUG_ASSERTCRASH(m_info->m_goalUnitID==INVALID_ID && m_info->m_posUnitID==INVALID_ID, ("Shouldn't be occupied.")); - DEBUG_ASSERTCRASH(m_info->m_goalAircraftID==INVALID_ID , ("Shouldn't be occupied by aircraft.")); - if (m_info->m_prevOpen || m_info->m_nextOpen || m_info->m_open || m_info->m_closed) { - // Bad release. Skip for now, better leak than crash. jba. - return; - } - - PathfindCellInfo::releaseACellInfo(m_info); - m_info = nullptr; - -} - -/** - * Sets the goal unit into the info record for a cell. - */ -void PathfindCell::setGoalUnit(ObjectID unitID, const ICoord2D &pos ) -{ - if (unitID==INVALID_ID) { - // removing goal. - if (m_info) { - m_info->m_goalUnitID = INVALID_ID; - if (m_info->m_posUnitID == INVALID_ID) { - // No units here. - DEBUG_ASSERTCRASH(m_flags==UNIT_GOAL, ("Bad flags.")); - m_flags = NO_UNITS; - releaseInfo(); - } else{ - m_flags = UNIT_PRESENT_MOVING; - } - } else { - DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); - } - } else { - // adding goal. - if (!m_info) { - DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); - allocateInfo(pos); - } - if (!m_info) { - DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); - return; - } - m_info->m_goalUnitID = unitID; - if (unitID==m_info->m_posUnitID) { - m_flags = UNIT_PRESENT_FIXED; - } else if (m_info->m_posUnitID==INVALID_ID) { - m_flags = UNIT_GOAL; - } else { - m_flags = UNIT_GOAL_OTHER_MOVING; - } - } -} - - -/** - * Sets the goal aircraft into the info record for a cell. - */ -void PathfindCell::setGoalAircraft(ObjectID unitID, const ICoord2D &pos ) -{ - if (unitID==INVALID_ID) { - // removing goal. - if (m_info) { - m_info->m_goalAircraftID = INVALID_ID; - m_aircraftGoal = false; - releaseInfo(); - } else { - DEBUG_ASSERTCRASH(m_aircraftGoal==false, ("Bad flags.")); - } - } else { - // adding goal. - if (!m_info) { - DEBUG_ASSERTCRASH(m_aircraftGoal==false, ("Bad flags.")); - allocateInfo(pos); - } - if (!m_info) { - DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); - return; - } - m_info->m_goalAircraftID = unitID; - m_aircraftGoal = true; - } -} - - -/** - * Sets the position unit into the info record for a cell. - */ -void PathfindCell::setPosUnit(ObjectID unitID, const ICoord2D &pos ) -{ - if (unitID==INVALID_ID) { - // removing position. - if (m_info) { - m_info->m_posUnitID = INVALID_ID; - if (m_info->m_goalUnitID == INVALID_ID) { - // No units here. - DEBUG_ASSERTCRASH(m_flags==UNIT_PRESENT_MOVING, ("Bad flags.")); - m_flags = NO_UNITS; - releaseInfo(); - } else { - m_flags = UNIT_GOAL; - } - } else { - DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); - } - } else { - // adding goal. - if (!m_info) { - DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); - allocateInfo(pos); - } - if (!m_info) { - DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); - return; - } - if (m_info->m_goalUnitID!=INVALID_ID && (m_info->m_goalUnitID==m_info->m_posUnitID)) { - // A unit is already occupying this cell. - return; - } - m_info->m_posUnitID = unitID; - if (unitID==m_info->m_goalUnitID) { - m_flags = UNIT_PRESENT_FIXED; - } else if (m_info->m_goalUnitID==INVALID_ID) { - m_flags = UNIT_PRESENT_MOVING; - } else { - m_flags = UNIT_GOAL_OTHER_MOVING; - } - } -} - - -/** - * Return the relevant obstacle ID. - */ -inline ObjectID PathfindCell::getObstacleID() const -{ -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - return m_obstacleID; - } - - return m_info ? m_info->m_obstacleID : INVALID_ID; -#else - return m_obstacleID; -#endif -} - - -/** - * Flag this cell as an obstacle, from the given one. - * Return true if cell was flagged. - */ -Bool PathfindCell::setTypeAsObstacle( Object *obstacle, Bool isFence, const ICoord2D &pos ) -{ - if (m_type!=PathfindCell::CELL_CLEAR && m_type != PathfindCell::CELL_IMPASSABLE) { - return false; - } - - Bool isRubble = false; - if (obstacle->getBodyModule() && obstacle->getBodyModule()->getDamageState() == BODY_RUBBLE) - { - isRubble = true; - } - - if (isRubble) { - m_type = PathfindCell::CELL_RUBBLE; - m_obstacleID = INVALID_ID; - m_obstacleIsFence = false; - m_obstacleIsTransparent = false; -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - return true; - } - - if (m_info) { - m_info->m_obstacleID = INVALID_ID; - releaseInfo(); - } -#endif - return true; - } - - m_type = PathfindCell::CELL_OBSTACLE; - m_obstacleID = obstacle->getID(); - m_obstacleIsFence = isFence; - m_obstacleIsTransparent = obstacle->isKindOf(KINDOF_CAN_SEE_THROUGH_STRUCTURE); -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - // TheSuperHackers @info In retail mode we need to track orphaned cells set as obstacles so we can cleanup and failover properly - // So we always make sure to set and clear the local obstacle data on the PathfindCell regardless of retail compat or not - if (s_useFixedPathfinding) { - return true; - } - - if (!m_info) { - m_info = PathfindCellInfo::getACellInfo(this, pos); - if (!m_info) { - DEBUG_CRASH(("Not enough PathFindCellInfos in pool.")); - return false; - } - } - m_info->m_obstacleID = obstacle->getID(); - m_info->m_obstacleIsFence = isFence; - m_info->m_obstacleIsTransparent = obstacle->isKindOf(KINDOF_CAN_SEE_THROUGH_STRUCTURE); -#endif - return true; -} - -/** - * Flag this cell as given type. - */ -void PathfindCell::setType( CellType type ) -{ -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - if (m_obstacleID != INVALID_ID) { - DEBUG_ASSERTCRASH(type == PathfindCell::CELL_OBSTACLE, ("Wrong type.")); - m_type = PathfindCell::CELL_OBSTACLE; - return; - } - } - - if (m_info && (m_info->m_obstacleID != INVALID_ID)) { - DEBUG_ASSERTCRASH(type==PathfindCell::CELL_OBSTACLE, ("Wrong type.")); - m_type = PathfindCell::CELL_OBSTACLE; - return; - } -#else - if (m_obstacleID != INVALID_ID) { - DEBUG_ASSERTCRASH(type == PathfindCell::CELL_OBSTACLE, ("Wrong type.")); - m_type = PathfindCell::CELL_OBSTACLE; - return; - } -#endif - m_type = type; -} - -/** - * Unflag this cell as an obstacle, from the given one. - * Return true if this cell was previously flagged as an obstacle by this object. - */ -Bool PathfindCell::removeObstacle( Object *obstacle ) -{ - if (m_type == PathfindCell::CELL_RUBBLE) { - m_type = PathfindCell::CELL_CLEAR; - } -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - if (m_obstacleID != obstacle->getID()) return false; - m_type = PathfindCell::CELL_CLEAR; - m_obstacleID = INVALID_ID; - m_obstacleIsFence = false; - m_obstacleIsTransparent = false; - return true; - } - - if (!m_info) return false; - if (m_info->m_obstacleID != obstacle->getID()) return false; - m_type = PathfindCell::CELL_CLEAR; - m_info->m_obstacleID = INVALID_ID; - releaseInfo(); - -#else - if (m_obstacleID != obstacle->getID()) return false; - m_type = PathfindCell::CELL_CLEAR; -#endif - m_obstacleID = INVALID_ID; - m_obstacleIsFence = false; - m_obstacleIsTransparent = false; - return true; -} - -#if RETAIL_COMPATIBLE_PATHFINDING -// Retail compatible insertion sort -void PathfindCell::forwardInsertionSortRetailCompatible(PathfindCellList& list) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(m_info->m_closed == FALSE && m_info->m_open == FALSE, ("Serious error - Invalid flags. jba")); - - // mark the newCell as being on the open list - m_info->m_open = true; - m_info->m_closed = false; - - if (list.m_head == nullptr) - { - list.m_head = this; - m_info->m_prevOpen = nullptr; - m_info->m_nextOpen = nullptr; - return; - } - - // insertion sort - PathfindCell* currentCell = list.m_head; - PathfindCell* previousCell = nullptr; - UnsignedInt cellCount = 0; - while (currentCell && cellCount < PATHFIND_CELLS_PER_FRAME && currentCell->m_info->m_totalCost <= m_info->m_totalCost) - { - // Prevent a retail crash where a pathfindCell has an m_info with a dangling nextOpen pointer - if (currentCell->m_info->m_nextOpen && !currentCell->m_info->m_nextOpen->m_cell->m_info) - { - currentCell->m_info->m_nextOpen->m_cell = nullptr; - currentCell->m_info->m_nextOpen = nullptr; - } - - cellCount++; - previousCell = currentCell; - currentCell = currentCell->getNextOpen(); - } - - if (currentCell) - { - // insert just before "currentCell" - if (currentCell->m_info->m_prevOpen) - currentCell->m_info->m_prevOpen->m_nextOpen = this->m_info; - else - list.m_head = this; - - m_info->m_prevOpen = currentCell->m_info->m_prevOpen; - currentCell->m_info->m_prevOpen = this->m_info; - - m_info->m_nextOpen = currentCell->m_info; - - } - else - { - // append after "previousCell" - we are at the end of the list - previousCell->m_info->m_nextOpen = this->m_info; - m_info->m_prevOpen = previousCell->m_info; - m_info->m_nextOpen = nullptr; - } -} -#endif - -// Forward insertion sort, returns early if the list is being initialized or we are prepending the list -void PathfindCell::forwardInsertionSort(PathfindCellList& list) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(m_info->m_closed == FALSE && m_info->m_open == FALSE, ("Serious error - Invalid flags. jba")); - - // mark the new cell as being on the open list - m_info->m_open = true; - m_info->m_closed = false; - - if (list.m_head == nullptr) { - m_info->m_prevOpen = nullptr; - m_info->m_nextOpen = nullptr; - list.m_head = this; - list.m_tail = this; - return; - } - - // If the node needs inserting before the current list head - if (m_info->m_totalCost < list.m_head->m_info->m_totalCost) { - m_info->m_prevOpen = nullptr; - list.m_head->m_info->m_prevOpen = this->m_info; - m_info->m_nextOpen = list.m_head->m_info; - list.m_head = this; - return; - } - - // Traverse the list to find correct position - PathfindCell* current = list.m_head; - while (current->m_info->m_nextOpen && current->m_info->m_nextOpen->m_totalCost <= m_info->m_totalCost) { - current = current->getNextOpen(); - } - - // Insert the new node in the correct position - m_info->m_nextOpen = current->m_info->m_nextOpen; - if (current->m_info->m_nextOpen != nullptr) { - current->m_info->m_nextOpen->m_prevOpen = this->m_info; - } - else { - list.m_tail = this; - } - - current->m_info->m_nextOpen = this->m_info; - m_info->m_prevOpen = current->m_info; -} - -// Reverse insertion sort, returns early if the list is being initialized or we are appending the list -void PathfindCell::reverseInsertionSort(PathfindCellList& list) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(m_info->m_closed == FALSE && m_info->m_open == FALSE, ("Serious error - Invalid flags. jba")); - - // mark the new cell as being on the open list - m_info->m_open = true; - m_info->m_closed = false; - - if (list.m_tail == nullptr) { - m_info->m_prevOpen = nullptr; - m_info->m_nextOpen = nullptr; - list.m_tail = this; - list.m_head = this; - return; - } - - // If the node needs inserting after the current list tail - if (m_info->m_totalCost >= list.m_tail->m_info->m_totalCost) { - m_info->m_prevOpen = list.m_tail->m_info; - list.m_tail->m_info->m_nextOpen = this->m_info; - m_info->m_nextOpen = nullptr; - list.m_tail = this; - return; - } - - // Traverse the list to find correct position - PathfindCell* current = list.m_tail; - while (current->m_info->m_prevOpen && current->m_info->m_prevOpen->m_totalCost > m_info->m_totalCost) { - current = current->getPrevOpen(); - } - - // Insert the new node in the correct position - m_info->m_prevOpen = current->m_info->m_prevOpen; - if (current->m_info->m_prevOpen != nullptr) { - current->m_info->m_prevOpen->m_nextOpen = this->m_info; - } - else { - list.m_head = this; - } - - current->m_info->m_prevOpen = this->m_info; - m_info->m_nextOpen = current->m_info; -} - -/// put self on "open" list in ascending cost order, return new list -void PathfindCell::putOnSortedOpenList( PathfindCellList &list ) -{ -#if RETAIL_COMPATIBLE_PATHFINDING - if (!s_useFixedPathfinding) { - forwardInsertionSortRetailCompatible(list); - return; - } -#endif - - // TheSuperHackers @performance Mauller 20/03/2026 Implement reverse insertion sorting. - // Long and complex paths often append PathfindCell's, with high total path costs, to the open list. - // Appending and reverse traversal allow faster insertion of these cells, reducing pathfinding overhead by 50 - 66%. - if (list.canReverseSort(*this)) { - reverseInsertionSort(list); - } - else { - forwardInsertionSort(list); - } -} - -/// remove self from "open" list -void PathfindCell::removeFromOpenList( PathfindCellList &list ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(m_info->m_closed==FALSE && m_info->m_open==TRUE, ("Serious error - Invalid flags. jba")); - if (m_info->m_nextOpen) - m_info->m_nextOpen->m_prevOpen = m_info->m_prevOpen; - else { - list.m_tail = getPrevOpen(); - } - - if (m_info->m_prevOpen) - m_info->m_prevOpen->m_nextOpen = m_info->m_nextOpen; - else - list.m_head = getNextOpen(); - - m_info->m_open = false; - m_info->m_nextOpen = nullptr; - m_info->m_prevOpen = nullptr; - -} - -/// remove all cells from "open" list -Int PathfindCell::releaseOpenList( PathfindCellList &list ) -{ - Int count = 0; - while (list.m_head) { - count++; - DEBUG_ASSERTCRASH(list.m_head->m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(list.m_head->m_info->m_closed==FALSE && list.m_head->m_info->m_open==TRUE, ("Serious error - Invalid flags. jba")); - PathfindCell *cur = list.m_head; - PathfindCellInfo *curInfo = list.m_head->m_info; - -#if RETAIL_COMPATIBLE_PATHFINDING - // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding - // One crash mode is where a cell has no PathfindCellInfo, resulting in a nullptr access and a crash. - // Therefore we signal that we need to clean the maps cells and the PathfindCellInfos - if(!curInfo && !s_useFixedPathfinding) { - s_useFixedPathfinding = true; - s_forceCleanCells = true; - return count; - } -#endif - - if (curInfo->m_nextOpen) { - list.m_head = curInfo->m_nextOpen->m_cell; - } else { - list.reset(); - } - DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); - curInfo->m_nextOpen = nullptr; - curInfo->m_prevOpen = nullptr; - curInfo->m_open = FALSE; - cur->releaseInfo(); - } - return count; -} - -/// remove all cells from "closed" list -Int PathfindCell::releaseClosedList( PathfindCellList &list ) -{ - Int count = 0; - while (list.m_head) { - count++; - DEBUG_ASSERTCRASH(list.m_head->m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(list.m_head->m_info->m_closed==TRUE && list.m_head->m_info->m_open==FALSE, ("Serious error - Invalid flags. jba")); - PathfindCell *cur = list.m_head; - PathfindCellInfo *curInfo = list.m_head->m_info; -#if RETAIL_COMPATIBLE_PATHFINDING - // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding - // One crash mode is where a cell has no PathfindCellInfo, resulting in a nullptr access and a crash. - // Therefore we signal that we need to clean the maps cells and the PathfindCellInfos - if(!curInfo && !s_useFixedPathfinding) { - s_useFixedPathfinding = true; - s_forceCleanCells = true; - return count; - } -#endif - - if (curInfo->m_nextOpen) { - list.m_head = curInfo->m_nextOpen->m_cell; - } else { - list.reset(); - } - DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); - curInfo->m_nextOpen = nullptr; - curInfo->m_prevOpen = nullptr; - curInfo->m_closed = FALSE; - cur->releaseInfo(); - } - return count; -} - -/// put self on "closed" list, return new list -void PathfindCell::putOnClosedList( PathfindCellList &list ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(m_info->m_closed==FALSE && m_info->m_open==FALSE, ("Serious error - Invalid flags. jba")); - // only put on list if not already on it - if (m_info->m_closed == FALSE) - { - m_info->m_closed = FALSE; - m_info->m_closed = TRUE; - - m_info->m_prevOpen = nullptr; - m_info->m_nextOpen = list.m_head ? list.m_head->m_info : nullptr; - if (list.m_head) -#if RETAIL_COMPATIBLE_PATHFINDING - // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding - // This crash mode occurs due to the closed list head not having an m_info associated with it - // A node cannot be put onto the closed list without an m_info under normal conditions - { - if (list.m_head->m_info) - { - list.m_head->m_info->m_prevOpen = this->m_info; - } - } -#else - list.m_head->m_info->m_prevOpen = this->m_info; -#endif - - list.m_head = this; - } - -} - -/// remove self from "closed" list -void PathfindCell::removeFromClosedList( PathfindCellList &list ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - DEBUG_ASSERTCRASH(m_info->m_closed==TRUE && m_info->m_open==FALSE, ("Serious error - Invalid flags. jba")); - if (m_info->m_nextOpen) - m_info->m_nextOpen->m_prevOpen = m_info->m_prevOpen; - - if (m_info->m_prevOpen) - m_info->m_prevOpen->m_nextOpen = m_info->m_nextOpen; - else - list.m_head = getNextOpen(); - - m_info->m_closed = false; - m_info->m_nextOpen = nullptr; - m_info->m_prevOpen = nullptr; - -} - -/** - * Return true if the given object ID is registered as an obstacle in this cell - */ -inline Bool PathfindCell::isObstaclePresent(ObjectID objID) const -{ - if (objID != INVALID_ID && (getType() == PathfindCell::CELL_OBSTACLE)) - { -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - return m_obstacleID == objID; - } - - DEBUG_ASSERTCRASH(m_info, ("Should have info to be obstacle.")); - return (m_info && m_info->m_obstacleID == objID); -#else - return m_obstacleID == objID; -#endif - } - - return false; -} - - -/** - * return true if the obstacle in the cell is KINDOF_CAN_SEE_THROUGHT_STRUCTURE - */ -inline Bool PathfindCell::isObstacleTransparent() const -{ -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - return m_obstacleIsTransparent; - } - - return m_info ? m_info->m_obstacleIsTransparent : false; -#else - return m_obstacleIsTransparent; -#endif -} - -/** - * return true if the given obstacle in the cell is a fence. - */ -inline Bool PathfindCell::isObstacleFence() const -{ -#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION - if (s_useFixedPathfinding) { - return m_obstacleIsFence; - } - - return m_info ? m_info->m_obstacleIsFence : false; -#else - return m_obstacleIsFence; -#endif -} - - - -UnsignedInt PathfindCell::costToGoal( PathfindCell *goal ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - Int dx = m_info->m_pos.x - goal->getXIndex(); - Int dy = m_info->m_pos.y - goal->getYIndex(); -#define NO_REAL_DIST -#ifdef REAL_DIST - Int cost = COST_ORTHOGONAL*sqrt(dx*dx + dy*dy); -#else - if (dx<0) dx = -dx; - if (dy<0) dy = -dy; - Int cost; - if (dx>dy) { - cost= COST_ORTHOGONAL*dx + (COST_ORTHOGONAL*dy)/2; - } else { - cost= COST_ORTHOGONAL*dy + (COST_ORTHOGONAL*dx)/2; - } - -#endif - - - return cost; -} - -UnsignedInt PathfindCell::costToHierGoal( PathfindCell *goal ) -{ - if( !m_info ) - { - DEBUG_CRASH( ("Has to have info.") ); - return 100000; //...patch hack 1.01 - } - Int dx = m_info->m_pos.x - goal->getXIndex(); - Int dy = m_info->m_pos.y - goal->getYIndex(); - Int cost = REAL_TO_INT_FLOOR(COST_ORTHOGONAL*sqrt(dx*dx + dy*dy) + 0.5f); - return cost; -} - -UnsignedInt PathfindCell::costSoFar( PathfindCell *parent ) -{ - DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); - // very first node in path - no turns, no cost - if (parent == nullptr) - return 0; - - // add in number of turns in path so far - ICoord2D prevDir; - Int cost; - - prevDir.x = parent->getXIndex() - m_info->m_pos.x; - prevDir.y = parent->getYIndex() - m_info->m_pos.y; - - // diagonal moves cost a bit more than orthogonal ones - if (prevDir.x == 0 || prevDir.y == 0) - cost = parent->getCostSoFar() + COST_ORTHOGONAL; - else - cost = parent->getCostSoFar() + COST_DIAGONAL; - if (getPinched()) { - cost += 1*COST_DIAGONAL; - } - -#if 1 - // Increase cost of turns. - Int numTurns = 0; - PathfindCell *prevCell = parent->getParentCell(); - if (prevCell) { - -#if RETAIL_COMPATIBLE_PATHFINDING - // TheSuperHackers @info this is a possible crash point in the retail pathfinding, we just prevent the crash at this point - // External code should catch the issue in another block and cleanup the pathfinding before switching to the fixed pathfinding. - if (!prevCell->hasInfo()) - { - return cost; - } -#endif - - ICoord2D dir; - dir.x = prevCell->getXIndex() - parent->getXIndex(); - dir.y = prevCell->getYIndex() - parent->getYIndex(); - - // count number of direction changes - if (dir.x != prevDir.x || dir.y != prevDir.y) - { - Int dot = dir.x * prevDir.x + dir.y * prevDir.y; - if (dot > 0) - numTurns=4; // 45 degree turn - else if (dot == 0) - numTurns = 8; // 90 degree turn - else - numTurns = 16; // 135 degree turn - } - } - - return cost + numTurns; -#else - return cost; -#endif - -} - - inline Bool typesMatch(const PathfindCell &targetCell, const PathfindCell &sourceCell) { PathfindCell::CellType targetType = targetCell.getType(); PathfindCell::CellType srcType = sourceCell.getType(); diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp new file mode 100644 index 00000000000..eae19b45694 --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp @@ -0,0 +1,954 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/Object.h" +#include "GameLogic/Pathfinder/PathfindCell.h" +#include "GameLogic/Pathfinder/PathfindCellInfo.h" +#include "GameLogic/Pathfinder/PathfindCellList.h" +#include "GameLogic/Pathfinder/PathfindConstants.h" +#include "GameLogic/Module/BodyModule.h" + +/** + * Constructor + */ +PathfindCell::PathfindCell() :m_info(nullptr) +{ + reset(); +} + +/** + * Destructor + */ +PathfindCell::~PathfindCell() +{ + if (m_info) PathfindCellInfo::releaseACellInfo(m_info); + m_info = nullptr; + static Bool warn = true; + if (warn) { + warn = false; + DEBUG_LOG( ("PathfindCell::~PathfindCell m_info Allocated.")); + } +} + +/** + * Reset the cell to default values + */ +void PathfindCell::reset() +{ + m_type = CELL_CLEAR; + m_flags = NO_UNITS; + m_zone = 0; + m_aircraftGoal = false; + m_pinched = false; + if (m_info) { + m_info->m_obstacleID = INVALID_ID; + PathfindCellInfo::releaseACellInfo(m_info); + m_info = nullptr; + } + m_obstacleID = INVALID_ID; + m_blockedByAlly = false; + m_obstacleIsFence = false; + m_obstacleIsTransparent = false; + + m_connectsToLayer = LAYER_INVALID; + m_layer = LAYER_GROUND; + +} + +/** + * Reset the pathfinding values in the cell. + */ +Bool PathfindCell::startPathfind( PathfindCell *goalCell ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + m_info->m_nextOpen = nullptr; + m_info->m_prevOpen = nullptr; + m_info->m_pathParent = nullptr; + m_info->m_costSoFar = 0; // start node, no cost to get here + m_info->m_totalCost = 0; + if (goalCell) { + m_info->m_totalCost = costToGoal( goalCell ); + } +#if RETAIL_COMPATIBLE_PATHFINDING + if (!s_useFixedPathfinding) { + m_info->m_open = TRUE; + } else +#endif + { + m_info->m_open = FALSE; + } + m_info->m_closed = FALSE; + return true; +} + +/** + * Set the blocked by ally flag on the pathfind cell info. + */ +Bool PathfindCell::isBlockedByAlly() const +{ +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + return m_blockedByAlly; + } + + return m_info->m_blockedByAlly; +#else + return m_blockedByAlly; +#endif +} + +void PathfindCell::setBlockedByAlly(Bool blocked) +{ +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + m_blockedByAlly = (blocked != 0); + return; + } + + m_info->m_blockedByAlly = (blocked != 0); +#else + m_blockedByAlly = (blocked != 0); +#endif +} + +/** + * Determine absolute total path cost difference between two cells. + * Returns UINT_MAX if used with an uninitialised cell, so will be sorted as maximally dissimilar. + */ +UnsignedInt PathfindCell::getTotalCostDifference(PathfindCell& other) const +{ + if (m_info && other.m_info) + return abs((Int)m_info->m_totalCost - (Int)other.m_info->m_totalCost); + + return UINT_MAX; +} + +/** + * Set the parent pointer. + */ +void PathfindCell::setParentCell( PathfindCell* parent ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + m_info->m_pathParent = parent->m_info; + Int dx = m_info->m_pos.x - parent->m_info->m_pos.x; + Int dy = m_info->m_pos.y - parent->m_info->m_pos.y; + if (dx<-1 || dx>1 || dy<-1 || dy>1) { + DEBUG_CRASH(("Invalid parent index.")); + } +} + +/** + * Set the parent pointer. + */ +void PathfindCell::setParentCellHierarchical( PathfindCell* parent ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + m_info->m_pathParent = parent->m_info; +} + +/** + * Reset the parent cell. + */ +void PathfindCell::clearParentCell( ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + m_info->m_pathParent = nullptr; +} + + +/** + * Allocates an info record for a cell. + */ +Bool PathfindCell::allocateInfo( const ICoord2D &pos ) +{ + if (!m_info) { + m_info = PathfindCellInfo::getACellInfo(this, pos); + return (m_info != nullptr); + } + return true; +} + +/** + * Releases an info record for a cell. + */ +void PathfindCell::releaseInfo() +{ + // TheSuperHackers @bugfix Mauller/SkyAero 05/06/2025 Parent cell links need clearing to prevent dangling pointers on starting points that can link them to an invalid parent cell. + // Parent cells are only cleared within Pathfinder::prependCells, so cells that do not make it onto the final path do not get their parent cell cleared. + // Cells with a special flags also do not get their PathfindCellInfo cleared and therefore can leave a parent cell set on a starting cell. +#if RETAIL_COMPATIBLE_PATHFINDING + if (s_useFixedPathfinding) +#endif + { + if (m_info) { + m_info->m_pathParent = nullptr; + } + } + + if (m_type == CELL_OBSTACLE || m_flags != NO_UNITS || m_aircraftGoal) { + return; + } + + if (!m_info) { + return; + } + + DEBUG_ASSERTCRASH(m_info->m_prevOpen==nullptr && m_info->m_nextOpen==nullptr, ("Shouldn't be linked.")); + DEBUG_ASSERTCRASH(m_info->m_open==0 && m_info->m_closed==0, ("Shouldn't be linked.")); + DEBUG_ASSERTCRASH(m_info->m_goalUnitID==INVALID_ID && m_info->m_posUnitID==INVALID_ID, ("Shouldn't be occupied.")); + DEBUG_ASSERTCRASH(m_info->m_goalAircraftID==INVALID_ID , ("Shouldn't be occupied by aircraft.")); + if (m_info->m_prevOpen || m_info->m_nextOpen || m_info->m_open || m_info->m_closed) { + // Bad release. Skip for now, better leak than crash. jba. + return; + } + + PathfindCellInfo::releaseACellInfo(m_info); + m_info = nullptr; + +} + +/** + * Sets the goal unit into the info record for a cell. + */ +void PathfindCell::setGoalUnit(ObjectID unitID, const ICoord2D &pos ) +{ + if (unitID==INVALID_ID) { + // removing goal. + if (m_info) { + m_info->m_goalUnitID = INVALID_ID; + if (m_info->m_posUnitID == INVALID_ID) { + // No units here. + DEBUG_ASSERTCRASH(m_flags==UNIT_GOAL, ("Bad flags.")); + m_flags = NO_UNITS; + releaseInfo(); + } else{ + m_flags = UNIT_PRESENT_MOVING; + } + } else { + DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); + } + } else { + // adding goal. + if (!m_info) { + DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); + allocateInfo(pos); + } + if (!m_info) { + DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); + return; + } + m_info->m_goalUnitID = unitID; + if (unitID==m_info->m_posUnitID) { + m_flags = UNIT_PRESENT_FIXED; + } else if (m_info->m_posUnitID==INVALID_ID) { + m_flags = UNIT_GOAL; + } else { + m_flags = UNIT_GOAL_OTHER_MOVING; + } + } +} + + +/** + * Sets the goal aircraft into the info record for a cell. + */ +void PathfindCell::setGoalAircraft(ObjectID unitID, const ICoord2D &pos ) +{ + if (unitID==INVALID_ID) { + // removing goal. + if (m_info) { + m_info->m_goalAircraftID = INVALID_ID; + m_aircraftGoal = false; + releaseInfo(); + } else { + DEBUG_ASSERTCRASH(m_aircraftGoal==false, ("Bad flags.")); + } + } else { + // adding goal. + if (!m_info) { + DEBUG_ASSERTCRASH(m_aircraftGoal==false, ("Bad flags.")); + allocateInfo(pos); + } + if (!m_info) { + DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); + return; + } + m_info->m_goalAircraftID = unitID; + m_aircraftGoal = true; + } +} + + +/** + * Sets the position unit into the info record for a cell. + */ +void PathfindCell::setPosUnit(ObjectID unitID, const ICoord2D &pos ) +{ + if (unitID==INVALID_ID) { + // removing position. + if (m_info) { + m_info->m_posUnitID = INVALID_ID; + if (m_info->m_goalUnitID == INVALID_ID) { + // No units here. + DEBUG_ASSERTCRASH(m_flags==UNIT_PRESENT_MOVING, ("Bad flags.")); + m_flags = NO_UNITS; + releaseInfo(); + } else { + m_flags = UNIT_GOAL; + } + } else { + DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); + } + } else { + // adding goal. + if (!m_info) { + DEBUG_ASSERTCRASH(m_flags == NO_UNITS, ("Bad flags.")); + allocateInfo(pos); + } + if (!m_info) { + DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); + return; + } + if (m_info->m_goalUnitID!=INVALID_ID && (m_info->m_goalUnitID==m_info->m_posUnitID)) { + // A unit is already occupying this cell. + return; + } + m_info->m_posUnitID = unitID; + if (unitID==m_info->m_goalUnitID) { + m_flags = UNIT_PRESENT_FIXED; + } else if (m_info->m_goalUnitID==INVALID_ID) { + m_flags = UNIT_PRESENT_MOVING; + } else { + m_flags = UNIT_GOAL_OTHER_MOVING; + } + } +} + + +/** + * Return the relevant obstacle ID. + */ +ObjectID PathfindCell::getObstacleID() const +{ +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + return m_obstacleID; + } + + return m_info ? m_info->m_obstacleID : INVALID_ID; +#else + return m_obstacleID; +#endif +} + + +/** + * Flag this cell as an obstacle, from the given one. + * Return true if cell was flagged. + */ +Bool PathfindCell::setTypeAsObstacle( Object *obstacle, Bool isFence, const ICoord2D &pos ) +{ + if (m_type!=CELL_CLEAR && m_type != CELL_IMPASSABLE) { + return false; + } + + Bool isRubble = false; + if (obstacle->getBodyModule() && obstacle->getBodyModule()->getDamageState() == BODY_RUBBLE) + { + isRubble = true; + } + + if (isRubble) { + m_type = CELL_RUBBLE; + m_obstacleID = INVALID_ID; + m_obstacleIsFence = false; + m_obstacleIsTransparent = false; +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + return true; + } + + if (m_info) { + m_info->m_obstacleID = INVALID_ID; + releaseInfo(); + } +#endif + return true; + } + + m_type = CELL_OBSTACLE; + m_obstacleID = obstacle->getID(); + m_obstacleIsFence = isFence; + m_obstacleIsTransparent = obstacle->isKindOf(KINDOF_CAN_SEE_THROUGH_STRUCTURE); +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + // TheSuperHackers @info In retail mode we need to track orphaned cells set as obstacles so we can cleanup and failover properly + // So we always make sure to set and clear the local obstacle data on the PathfindCell regardless of retail compat or not + if (s_useFixedPathfinding) { + return true; + } + + if (!m_info) { + m_info = PathfindCellInfo::getACellInfo(this, pos); + if (!m_info) { + DEBUG_CRASH(("Not enough PathFindCellInfos in pool.")); + return false; + } + } + m_info->m_obstacleID = obstacle->getID(); + m_info->m_obstacleIsFence = isFence; + m_info->m_obstacleIsTransparent = obstacle->isKindOf(KINDOF_CAN_SEE_THROUGH_STRUCTURE); +#endif + return true; +} + +/** + * Flag this cell as given type. + */ +void PathfindCell::setType( CellType type ) +{ +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + if (m_obstacleID != INVALID_ID) { + DEBUG_ASSERTCRASH(type == CELL_OBSTACLE, ("Wrong type.")); + m_type = CELL_OBSTACLE; + return; + } + } + + if (m_info && (m_info->m_obstacleID != INVALID_ID)) { + DEBUG_ASSERTCRASH(type==CELL_OBSTACLE, ("Wrong type.")); + m_type = CELL_OBSTACLE; + return; + } +#else + if (m_obstacleID != INVALID_ID) { + DEBUG_ASSERTCRASH(type == CELL_OBSTACLE, ("Wrong type.")); + m_type = CELL_OBSTACLE; + return; + } +#endif + m_type = type; +} + +/** + * Unflag this cell as an obstacle, from the given one. + * Return true if this cell was previously flagged as an obstacle by this object. + */ +Bool PathfindCell::removeObstacle( Object *obstacle ) +{ + if (m_type == CELL_RUBBLE) { + m_type = CELL_CLEAR; + } +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + if (m_obstacleID != obstacle->getID()) return false; + m_type = CELL_CLEAR; + m_obstacleID = INVALID_ID; + m_obstacleIsFence = false; + m_obstacleIsTransparent = false; + return true; + } + + if (!m_info) return false; + if (m_info->m_obstacleID != obstacle->getID()) return false; + m_type = CELL_CLEAR; + m_info->m_obstacleID = INVALID_ID; + releaseInfo(); + +#else + if (m_obstacleID != obstacle->getID()) return false; + m_type = CELL_CLEAR; +#endif + m_obstacleID = INVALID_ID; + m_obstacleIsFence = false; + m_obstacleIsTransparent = false; + return true; +} + +#if RETAIL_COMPATIBLE_PATHFINDING +// Retail compatible insertion sort +void PathfindCell::forwardInsertionSortRetailCompatible(PathfindCellList& list) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(m_info->m_closed == FALSE && m_info->m_open == FALSE, ("Serious error - Invalid flags. jba")); + + // mark the newCell as being on the open list + m_info->m_open = true; + m_info->m_closed = false; + + if (list.m_head == nullptr) + { + list.m_head = this; + m_info->m_prevOpen = nullptr; + m_info->m_nextOpen = nullptr; + return; + } + + // insertion sort + PathfindCell* currentCell = list.m_head; + PathfindCell* previousCell = nullptr; + UnsignedInt cellCount = 0; + while (currentCell && cellCount < PATHFIND_CELLS_PER_FRAME && currentCell->m_info->m_totalCost <= m_info->m_totalCost) + { + // Prevent a retail crash where a pathfindCell has an m_info with a dangling nextOpen pointer + if (currentCell->m_info->m_nextOpen && !currentCell->m_info->m_nextOpen->m_cell->m_info) + { + currentCell->m_info->m_nextOpen->m_cell = nullptr; + currentCell->m_info->m_nextOpen = nullptr; + } + + cellCount++; + previousCell = currentCell; + currentCell = currentCell->getNextOpen(); + } + + if (currentCell) + { + // insert just before "currentCell" + if (currentCell->m_info->m_prevOpen) + currentCell->m_info->m_prevOpen->m_nextOpen = this->m_info; + else + list.m_head = this; + + m_info->m_prevOpen = currentCell->m_info->m_prevOpen; + currentCell->m_info->m_prevOpen = this->m_info; + + m_info->m_nextOpen = currentCell->m_info; + + } + else + { + // append after "previousCell" - we are at the end of the list + previousCell->m_info->m_nextOpen = this->m_info; + m_info->m_prevOpen = previousCell->m_info; + m_info->m_nextOpen = nullptr; + } +} +#endif + +// Forward insertion sort, returns early if the list is being initialized or we are prepending the list +void PathfindCell::forwardInsertionSort(PathfindCellList& list) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(m_info->m_closed == FALSE && m_info->m_open == FALSE, ("Serious error - Invalid flags. jba")); + + // mark the new cell as being on the open list + m_info->m_open = true; + m_info->m_closed = false; + + if (list.m_head == nullptr) { + m_info->m_prevOpen = nullptr; + m_info->m_nextOpen = nullptr; + list.m_head = this; + list.m_tail = this; + return; + } + + // If the node needs inserting before the current list head + if (m_info->m_totalCost < list.m_head->m_info->m_totalCost) { + m_info->m_prevOpen = nullptr; + list.m_head->m_info->m_prevOpen = this->m_info; + m_info->m_nextOpen = list.m_head->m_info; + list.m_head = this; + return; + } + + // Traverse the list to find correct position + PathfindCell* current = list.m_head; + while (current->m_info->m_nextOpen && current->m_info->m_nextOpen->m_totalCost <= m_info->m_totalCost) { + current = current->getNextOpen(); + } + + // Insert the new node in the correct position + m_info->m_nextOpen = current->m_info->m_nextOpen; + if (current->m_info->m_nextOpen != nullptr) { + current->m_info->m_nextOpen->m_prevOpen = this->m_info; + } + else { + list.m_tail = this; + } + + current->m_info->m_nextOpen = this->m_info; + m_info->m_prevOpen = current->m_info; +} + +// Reverse insertion sort, returns early if the list is being initialized or we are appending the list +void PathfindCell::reverseInsertionSort(PathfindCellList& list) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(m_info->m_closed == FALSE && m_info->m_open == FALSE, ("Serious error - Invalid flags. jba")); + + // mark the new cell as being on the open list + m_info->m_open = true; + m_info->m_closed = false; + + if (list.m_tail == nullptr) { + m_info->m_prevOpen = nullptr; + m_info->m_nextOpen = nullptr; + list.m_tail = this; + list.m_head = this; + return; + } + + // If the node needs inserting after the current list tail + if (m_info->m_totalCost >= list.m_tail->m_info->m_totalCost) { + m_info->m_prevOpen = list.m_tail->m_info; + list.m_tail->m_info->m_nextOpen = this->m_info; + m_info->m_nextOpen = nullptr; + list.m_tail = this; + return; + } + + // Traverse the list to find correct position + PathfindCell* current = list.m_tail; + while (current->m_info->m_prevOpen && current->m_info->m_prevOpen->m_totalCost > m_info->m_totalCost) { + current = current->getPrevOpen(); + } + + // Insert the new node in the correct position + m_info->m_prevOpen = current->m_info->m_prevOpen; + if (current->m_info->m_prevOpen != nullptr) { + current->m_info->m_prevOpen->m_nextOpen = this->m_info; + } + else { + list.m_head = this; + } + + current->m_info->m_prevOpen = this->m_info; + m_info->m_nextOpen = current->m_info; +} + +/// put self on "open" list in ascending cost order, return new list +void PathfindCell::putOnSortedOpenList( PathfindCellList &list ) +{ +#if RETAIL_COMPATIBLE_PATHFINDING + if (!s_useFixedPathfinding) { + forwardInsertionSortRetailCompatible(list); + return; + } +#endif + + // TheSuperHackers @performance Mauller 20/03/2026 Implement reverse insertion sorting. + // Long and complex paths often append PathfindCell's, with high total path costs, to the open list. + // Appending and reverse traversal allow faster insertion of these cells, reducing pathfinding overhead by 50 - 66%. + if (list.canReverseSort(*this)) { + reverseInsertionSort(list); + } + else { + forwardInsertionSort(list); + } +} + +/// remove self from "open" list +void PathfindCell::removeFromOpenList( PathfindCellList &list ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(m_info->m_closed==FALSE && m_info->m_open==TRUE, ("Serious error - Invalid flags. jba")); + if (m_info->m_nextOpen) + m_info->m_nextOpen->m_prevOpen = m_info->m_prevOpen; + else { + list.m_tail = getPrevOpen(); + } + + if (m_info->m_prevOpen) + m_info->m_prevOpen->m_nextOpen = m_info->m_nextOpen; + else + list.m_head = getNextOpen(); + + m_info->m_open = false; + m_info->m_nextOpen = nullptr; + m_info->m_prevOpen = nullptr; + +} + +/// remove all cells from "open" list +Int PathfindCell::releaseOpenList( PathfindCellList &list ) +{ + Int count = 0; + while (list.m_head) { + count++; + DEBUG_ASSERTCRASH(list.m_head->m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(list.m_head->m_info->m_closed==FALSE && list.m_head->m_info->m_open==TRUE, ("Serious error - Invalid flags. jba")); + PathfindCell *cur = list.m_head; + PathfindCellInfo *curInfo = list.m_head->m_info; + +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding + // One crash mode is where a cell has no PathfindCellInfo, resulting in a nullptr access and a crash. + // Therefore we signal that we need to clean the maps cells and the PathfindCellInfos + if(!curInfo && !s_useFixedPathfinding) { + s_useFixedPathfinding = true; + s_forceCleanCells = true; + return count; + } +#endif + + if (curInfo->m_nextOpen) { + list.m_head = curInfo->m_nextOpen->m_cell; + } else { + list.reset(); + } + DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); + curInfo->m_nextOpen = nullptr; + curInfo->m_prevOpen = nullptr; + curInfo->m_open = FALSE; + cur->releaseInfo(); + } + return count; +} + +/// remove all cells from "closed" list +Int PathfindCell::releaseClosedList( PathfindCellList &list ) +{ + Int count = 0; + while (list.m_head) { + count++; + DEBUG_ASSERTCRASH(list.m_head->m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(list.m_head->m_info->m_closed==TRUE && list.m_head->m_info->m_open==FALSE, ("Serious error - Invalid flags. jba")); + PathfindCell *cur = list.m_head; + PathfindCellInfo *curInfo = list.m_head->m_info; +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding + // One crash mode is where a cell has no PathfindCellInfo, resulting in a nullptr access and a crash. + // Therefore we signal that we need to clean the maps cells and the PathfindCellInfos + if(!curInfo && !s_useFixedPathfinding) { + s_useFixedPathfinding = true; + s_forceCleanCells = true; + return count; + } +#endif + + if (curInfo->m_nextOpen) { + list.m_head = curInfo->m_nextOpen->m_cell; + } else { + list.reset(); + } + DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); + curInfo->m_nextOpen = nullptr; + curInfo->m_prevOpen = nullptr; + curInfo->m_closed = FALSE; + cur->releaseInfo(); + } + return count; +} + +/// put self on "closed" list, return new list +void PathfindCell::putOnClosedList( PathfindCellList &list ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(m_info->m_closed==FALSE && m_info->m_open==FALSE, ("Serious error - Invalid flags. jba")); + // only put on list if not already on it + if (m_info->m_closed == FALSE) + { + m_info->m_closed = FALSE; + m_info->m_closed = TRUE; + + m_info->m_prevOpen = nullptr; + m_info->m_nextOpen = list.m_head ? list.m_head->m_info : nullptr; + if (list.m_head) +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding + // This crash mode occurs due to the closed list head not having an m_info associated with it + // A node cannot be put onto the closed list without an m_info under normal conditions + { + if (list.m_head->m_info) + { + list.m_head->m_info->m_prevOpen = this->m_info; + } + } +#else + list.m_head->m_info->m_prevOpen = this->m_info; +#endif + + list.m_head = this; + } + +} + +/// remove self from "closed" list +void PathfindCell::removeFromClosedList( PathfindCellList &list ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(m_info->m_closed==TRUE && m_info->m_open==FALSE, ("Serious error - Invalid flags. jba")); + if (m_info->m_nextOpen) + m_info->m_nextOpen->m_prevOpen = m_info->m_prevOpen; + + if (m_info->m_prevOpen) + m_info->m_prevOpen->m_nextOpen = m_info->m_nextOpen; + else + list.m_head = getNextOpen(); + + m_info->m_closed = false; + m_info->m_nextOpen = nullptr; + m_info->m_prevOpen = nullptr; + +} + +/** + * Return true if the given object ID is registered as an obstacle in this cell + */ +Bool PathfindCell::isObstaclePresent(ObjectID objID) const +{ + if (objID != INVALID_ID && (getType() == CELL_OBSTACLE)) + { +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + return m_obstacleID == objID; + } + + DEBUG_ASSERTCRASH(m_info, ("Should have info to be obstacle.")); + return (m_info && m_info->m_obstacleID == objID); +#else + return m_obstacleID == objID; +#endif + } + + return false; +} + + +/** + * return true if the obstacle in the cell is KINDOF_CAN_SEE_THROUGHT_STRUCTURE + */ +Bool PathfindCell::isObstacleTransparent() const +{ +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + return m_obstacleIsTransparent; + } + + return m_info ? m_info->m_obstacleIsTransparent : false; +#else + return m_obstacleIsTransparent; +#endif +} + +/** + * return true if the given obstacle in the cell is a fence. + */ +Bool PathfindCell::isObstacleFence() const +{ +#if RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION + if (s_useFixedPathfinding) { + return m_obstacleIsFence; + } + + return m_info ? m_info->m_obstacleIsFence : false; +#else + return m_obstacleIsFence; +#endif +} + +UnsignedInt PathfindCell::costToGoal( PathfindCell *goal ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + Int dx = m_info->m_pos.x - goal->getXIndex(); + Int dy = m_info->m_pos.y - goal->getYIndex(); +#define NO_REAL_DIST +#ifdef REAL_DIST + Int cost = COST_ORTHOGONAL*sqrt(dx*dx + dy*dy); +#else + if (dx<0) dx = -dx; + if (dy<0) dy = -dy; + Int cost; + if (dx>dy) { + cost= COST_ORTHOGONAL*dx + (COST_ORTHOGONAL*dy)/2; + } else { + cost= COST_ORTHOGONAL*dy + (COST_ORTHOGONAL*dx)/2; + } + +#endif + + + return cost; +} + +UnsignedInt PathfindCell::costToHierGoal( PathfindCell *goal ) +{ + if( !m_info ) + { + DEBUG_CRASH( ("Has to have info.") ); + return 100000; //...patch hack 1.01 + } + Int dx = m_info->m_pos.x - goal->getXIndex(); + Int dy = m_info->m_pos.y - goal->getYIndex(); + Int cost = REAL_TO_INT_FLOOR(COST_ORTHOGONAL*sqrt(dx*dx + dy*dy) + 0.5f); + return cost; +} + +UnsignedInt PathfindCell::costSoFar( PathfindCell *parent ) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + // very first node in path - no turns, no cost + if (parent == nullptr) + return 0; + + // add in number of turns in path so far + ICoord2D prevDir; + Int cost; + + prevDir.x = parent->getXIndex() - m_info->m_pos.x; + prevDir.y = parent->getYIndex() - m_info->m_pos.y; + + // diagonal moves cost a bit more than orthogonal ones + if (prevDir.x == 0 || prevDir.y == 0) + cost = parent->getCostSoFar() + COST_ORTHOGONAL; + else + cost = parent->getCostSoFar() + COST_DIAGONAL; + if (getPinched()) { + cost += 1*COST_DIAGONAL; + } + +#if 1 + // Increase cost of turns. + Int numTurns = 0; + PathfindCell *prevCell = parent->getParentCell(); + if (prevCell) { + +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info this is a possible crash point in the retail pathfinding, we just prevent the crash at this point + // External code should catch the issue in another block and cleanup the pathfinding before switching to the fixed pathfinding. + if (!prevCell->hasInfo()) + { + return cost; + } +#endif + + ICoord2D dir; + dir.x = prevCell->getXIndex() - parent->getXIndex(); + dir.y = prevCell->getYIndex() - parent->getYIndex(); + + // count number of direction changes + if (dir.x != prevDir.x || dir.y != prevDir.y) + { + Int dot = dir.x * prevDir.x + dir.y * prevDir.y; + if (dot > 0) + numTurns=4; // 45 degree turn + else if (dot == 0) + numTurns = 8; // 90 degree turn + else + numTurns = 16; // 135 degree turn + } + } + + return cost + numTurns; +#else + return cost; +#endif + +} From 041d03e510f0ba6a8a85b04887b615a9f8bf6b6d Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:27:10 +0200 Subject: [PATCH 06/10] refactor(pathfinder): Move PathfindCellList implementation to its own file (#3429) --- Core/GameEngine/CMakeLists.txt | 1 + .../Source/GameLogic/AI/AIPathfind.cpp | 10 ------- .../AI/Pathfinder/PathfindCellList.cpp | 28 +++++++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 1e193f26ddd..02959b12691 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -866,6 +866,7 @@ set(GAMEENGINE_SRC Source/GameLogic/AI/Pathfinder/Path.cpp Source/GameLogic/AI/Pathfinder/PathfindCell.cpp Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp + Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp # Source/GameLogic/AI/Squad.cpp diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 8f2d572c3c5..4899c2a2b1e 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -147,16 +147,6 @@ void Pathfinder::forceCleanCells() } #endif -//----------------------------------------------------------------------------------- - -Bool PathfindCellList::canReverseSort(PathfindCell& currentCell) const -{ - if (m_head && m_tail) - return m_head->getTotalCostDifference(currentCell) > m_tail->getTotalCostDifference(currentCell); - - return false; -} - inline Bool typesMatch(const PathfindCell &targetCell, const PathfindCell &sourceCell) { PathfindCell::CellType targetType = targetCell.getType(); PathfindCell::CellType srcType = sourceCell.getType(); diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp new file mode 100644 index 00000000000..4be96d3b39e --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp @@ -0,0 +1,28 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/Pathfinder/PathfindCell.h" +#include "GameLogic/Pathfinder/PathfindCellList.h" + +Bool PathfindCellList::canReverseSort(PathfindCell& currentCell) const +{ + if (m_head && m_tail) + return m_head->getTotalCostDifference(currentCell) > m_tail->getTotalCostDifference(currentCell); + + return false; +} From 4f92b89491199ed643bc5dc71b6f7d1983bf7c41 Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:46:17 +0200 Subject: [PATCH 07/10] refactor(pathfinder): Move cell helpers into PathfindCell (#3429) Move cell type-comparison predicates into `PathfindCell` as static methods and update `AIPathfind.cpp` zone and block calculations to use the class-scoped helpers. --- .../GameLogic/Pathfinder/PathfindCell.h | 7 + .../Source/GameLogic/AI/AIPathfind.cpp | 148 ++++-------------- .../GameLogic/AI/Pathfinder/PathfindCell.cpp | 112 +++++++++++++ 3 files changed, 151 insertions(+), 116 deletions(-) diff --git a/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h b/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h index a675ab807a9..6986ca6eb61 100644 --- a/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h +++ b/Core/GameEngine/Include/GameLogic/Pathfinder/PathfindCell.h @@ -166,6 +166,13 @@ class PathfindCell void setConnectLayer( PathfindLayerEnum layer ) { m_connectsToLayer = layer; } ///< set the cell layer connect id PathfindLayerEnum getConnectLayer() const { return (PathfindLayerEnum)m_connectsToLayer; } ///< get the cell layer connect id + static Bool typesMatch(const PathfindCell& targetCell, const PathfindCell& sourceCell); + static Bool waterGround(const PathfindCell& targetCell, const PathfindCell& sourceCell); + static Bool groundRubble(const PathfindCell& targetCell, const PathfindCell& sourceCell); + static Bool terrain(const PathfindCell& targetCell, const PathfindCell& sourceCell); + static Bool crusherGround(const PathfindCell& targetCell, const PathfindCell& sourceCell); + static Bool groundCliff(const PathfindCell& targetCell, const PathfindCell& sourceCell); + private: PathfindCellInfo *m_info; ObjectID m_obstacleID; ///< the object ID who overlaps this cell diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 4899c2a2b1e..488bc7cce91 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -147,90 +147,6 @@ void Pathfinder::forceCleanCells() } #endif -inline Bool typesMatch(const PathfindCell &targetCell, const PathfindCell &sourceCell) { - PathfindCell::CellType targetType = targetCell.getType(); - PathfindCell::CellType srcType = sourceCell.getType(); - if (targetType == srcType) return true; - - return false; -} - -inline Bool waterGround(const PathfindCell &targetCell, const PathfindCell &sourceCell) { - PathfindCell::CellType targetType = targetCell.getType(); - PathfindCell::CellType srcType = sourceCell.getType(); - if ( (targetType==PathfindCell::CELL_CLEAR && - (srcType&PathfindCell::CELL_WATER ))) { - return true; - } - if ( (srcType==PathfindCell::CELL_CLEAR && - (targetType&PathfindCell::CELL_WATER ))) { - return true; - } - - return false; -} - -inline Bool groundRubble(const PathfindCell &targetCell, const PathfindCell &sourceCell) { - PathfindCell::CellType targetType = targetCell.getType(); - PathfindCell::CellType srcType = sourceCell.getType(); - if ( (targetType==PathfindCell::CELL_CLEAR && - (srcType==PathfindCell::CELL_RUBBLE ))) { - return true; - } - if ( (srcType==PathfindCell::CELL_CLEAR && - (targetType==PathfindCell::CELL_RUBBLE ))) { - return true; - } - - return false; -} - -inline Bool terrain(const PathfindCell &targetCell, const PathfindCell &sourceCell) { - Int targetType = targetCell.getType(); - Int srcType = sourceCell.getType(); - if (targetType == PathfindCell::CELL_OBSTACLE) targetType = PathfindCell::CELL_CLEAR; - if (srcType == PathfindCell::CELL_OBSTACLE) srcType = PathfindCell::CELL_CLEAR; - if (targetType==srcType) { - return true; - } - return false; -} - -inline Bool crusherGround(const PathfindCell &targetCell, const PathfindCell &sourceCell) { - Int targetType = targetCell.getType(); - Int srcType = sourceCell.getType(); - if (targetType==PathfindCell::CELL_OBSTACLE) { - if (targetCell.isObstacleFence()) { - if (srcType == PathfindCell::CELL_CLEAR) { - return true; - } - } - } - if (srcType==PathfindCell::CELL_OBSTACLE) { - if (sourceCell.isObstacleFence()) { - if (targetType == PathfindCell::CELL_CLEAR) { - return true; - } - } - } - return false; -} - -inline Bool groundCliff(const PathfindCell &targetCell, const PathfindCell &sourceCell) { - PathfindCell::CellType targetType = targetCell.getType(); - PathfindCell::CellType srcType = sourceCell.getType(); - - if ( (targetType==PathfindCell::CELL_CLIFF ) && - (srcType==PathfindCell::CELL_CLEAR) ) { - return true; - } - if ( (targetType==PathfindCell::CELL_CLEAR ) && - (srcType==PathfindCell::CELL_CLIFF) ) { - return true; - } - return false; -} - static void __fastcall resolveBlockZones(Int srcZone, Int targetZone, zoneStorageType *zoneEquivalency, Int sizeOfZE) { Int i; @@ -400,30 +316,30 @@ void ZoneBlock::blockCalculateZones(PathfindCell **map, PathfindLayer layers[], for( i=bounds.lo.x; i<=bounds.hi.x; i++ ) { if (i>bounds.lo.x && map[i][j].getZone()!=map[i-1][j].getZone()) { - if (waterGround(map[i][j], map[i-1][j])) { + if (PathfindCell::waterGround(map[i][j], map[i-1][j])) { applyBlockZone(map[i][j], map[i-1][j], m_groundWaterZones, m_firstZone, m_numZones); } - if (groundRubble(map[i][j], map[i-1][j])) { + if (PathfindCell::groundRubble(map[i][j], map[i-1][j])) { applyBlockZone(map[i][j], map[i-1][j], m_groundRubbleZones, m_firstZone, m_numZones); } - if (groundCliff(map[i][j], map[i-1][j])) { + if (PathfindCell::groundCliff(map[i][j], map[i-1][j])) { applyBlockZone(map[i][j], map[i-1][j], m_groundCliffZones, m_firstZone, m_numZones); } - if (crusherGround(map[i][j], map[i-1][j])) { + if (PathfindCell::crusherGround(map[i][j], map[i-1][j])) { applyBlockZone(map[i][j], map[i-1][j], m_crusherZones, m_firstZone, m_numZones); } } if (j>bounds.lo.y && map[i][j].getZone()!=map[i][j-1].getZone()) { - if (waterGround(map[i][j],map[i][j-1])) { + if (PathfindCell::waterGround(map[i][j],map[i][j-1])) { applyBlockZone(map[i][j], map[i][j-1], m_groundWaterZones, m_firstZone, m_numZones); } - if (groundRubble(map[i][j], map[i][j-1])) { + if (PathfindCell::groundRubble(map[i][j], map[i][j-1])) { applyBlockZone(map[i][j], map[i][j-1], m_groundRubbleZones, m_firstZone, m_numZones); } - if (groundCliff(map[i][j],map[i][j-1])) { + if (PathfindCell::groundCliff(map[i][j],map[i][j-1])) { applyBlockZone(map[i][j], map[i][j-1], m_groundCliffZones, m_firstZone, m_numZones); } - if (crusherGround(map[i][j], map[i][j-1])) { + if (PathfindCell::crusherGround(map[i][j], map[i][j-1])) { applyBlockZone(map[i][j], map[i][j-1], m_crusherZones, m_firstZone, m_numZones); } } @@ -824,19 +740,19 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye if (r_thisCell.getType() == r_leftCell.getType()) { applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); } - if (waterGround(r_thisCell, r_leftCell)) { + if (PathfindCell::waterGround(r_thisCell, r_leftCell)) { applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); } - if (groundRubble(r_thisCell, r_leftCell)) { + if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) { applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); } - if (groundCliff(r_thisCell, r_leftCell)) { + if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) { applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); } - if (terrain(r_thisCell, r_leftCell)) { + if (PathfindCell::terrain(r_thisCell, r_leftCell)) { applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); } - if (crusherGround(r_thisCell, r_leftCell)) { + if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); } #else @@ -846,22 +762,22 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye else { Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below - if (terrain(r_thisCell, r_leftCell)) { + if (PathfindCell::terrain(r_thisCell, r_leftCell)) { applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); notTerrainOrCrusher = FALSE; } - if (crusherGround(r_thisCell, r_leftCell)) { + if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); notTerrainOrCrusher = FALSE; } if ( notTerrainOrCrusher ) { - if (waterGround(r_thisCell, r_leftCell)) + if (PathfindCell::waterGround(r_thisCell, r_leftCell)) applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); - else if (groundRubble(r_thisCell, r_leftCell)) + else if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); - else if (groundCliff(r_thisCell, r_leftCell)) + else if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); } @@ -877,19 +793,19 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye if (r_thisCell.getType() == r_topCell.getType()) { applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); } - if (waterGround(r_thisCell, r_topCell)) { + if (PathfindCell::waterGround(r_thisCell, r_topCell)) { applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); } - if (groundRubble(r_thisCell, r_topCell)) { + if (PathfindCell::groundRubble(r_thisCell, r_topCell)) { applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); } - if (groundCliff(r_thisCell, r_topCell)) { + if (PathfindCell::groundCliff(r_thisCell, r_topCell)) { applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); } - if (terrain(r_thisCell, r_topCell)) { + if (PathfindCell::terrain(r_thisCell, r_topCell)) { applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); } - if (crusherGround(r_thisCell, r_topCell)) { + if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); } #else @@ -899,22 +815,22 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye else { Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below - if (terrain(r_thisCell, r_topCell)) { + if (PathfindCell::terrain(r_thisCell, r_topCell)) { applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); notTerrainOrCrusher = FALSE; } - if (crusherGround(r_thisCell, r_topCell)) { + if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); notTerrainOrCrusher = FALSE; } if (notTerrainOrCrusher) { - if (waterGround(r_thisCell, r_topCell)) + if (PathfindCell::waterGround(r_thisCell, r_topCell)) applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); - else if (groundRubble(r_thisCell, r_topCell)) + else if (PathfindCell::groundRubble(r_thisCell, r_topCell)) applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); - else if (groundCliff(r_thisCell, r_topCell)) + else if (PathfindCell::groundCliff(r_thisCell, r_topCell)) applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); } @@ -1038,8 +954,8 @@ void PathfindZoneManager::updateZonesForModify(PathfindCell **map, PathfindLayer if (cell->getZone()!=UNINITIALIZED_ZONE) continue; } if (isetZone(map[i+1][j-1].getZone()); if (cell->getZone()!=UNINITIALIZED_ZONE) continue; } @@ -1063,8 +979,8 @@ void PathfindZoneManager::updateZonesForModify(PathfindCell **map, PathfindLayer if (cell->getZone()!=UNINITIALIZED_ZONE) continue; } if (isetZone(map[i+1][j+1].getZone()); if (cell->getZone()!=UNINITIALIZED_ZONE) continue; } diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp index eae19b45694..4c3d16c8520 100644 --- a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindCell.cpp @@ -952,3 +952,115 @@ UnsignedInt PathfindCell::costSoFar( PathfindCell *parent ) #endif } + +Bool PathfindCell::typesMatch(const PathfindCell& targetCell, const PathfindCell& sourceCell) +{ + CellType targetType = targetCell.getType(); + CellType srcType = sourceCell.getType(); + if (targetType == srcType) + { + return true; + } + + return false; +} + +Bool PathfindCell::waterGround(const PathfindCell& targetCell, const PathfindCell& sourceCell) +{ + CellType targetType = targetCell.getType(); + CellType srcType = sourceCell.getType(); + if ((targetType == CELL_CLEAR && + (srcType & CELL_WATER))) + { + return true; + } + if ((srcType == CELL_CLEAR && + (targetType & CELL_WATER))) + { + return true; + } + + return false; +} + +Bool PathfindCell::groundRubble(const PathfindCell& targetCell, const PathfindCell& sourceCell) +{ + CellType targetType = targetCell.getType(); + CellType srcType = sourceCell.getType(); + if ((targetType == CELL_CLEAR && + (srcType == CELL_RUBBLE))) + { + return true; + } + if ((srcType == CELL_CLEAR && + (targetType == CELL_RUBBLE))) + { + return true; + } + + return false; +} + +Bool PathfindCell::terrain(const PathfindCell& targetCell, const PathfindCell& sourceCell) +{ + Int targetType = targetCell.getType(); + Int srcType = sourceCell.getType(); + if (targetType == CELL_OBSTACLE) + { + targetType = CELL_CLEAR; + } + if (srcType == CELL_OBSTACLE) + { + srcType = CELL_CLEAR; + } + if (targetType == srcType) + { + return true; + } + return false; +} + +Bool PathfindCell::crusherGround(const PathfindCell& targetCell, const PathfindCell& sourceCell) +{ + Int targetType = targetCell.getType(); + Int srcType = sourceCell.getType(); + if (targetType == CELL_OBSTACLE) + { + if (targetCell.isObstacleFence()) + { + if (srcType == CELL_CLEAR) + { + return true; + } + } + } + if (srcType == CELL_OBSTACLE) + { + if (sourceCell.isObstacleFence()) + { + if (targetType == CELL_CLEAR) + { + return true; + } + } + } + return false; +} + +Bool PathfindCell::groundCliff(const PathfindCell& targetCell, const PathfindCell& sourceCell) +{ + CellType targetType = targetCell.getType(); + CellType srcType = sourceCell.getType(); + + if ((targetType == CELL_CLIFF) && + (srcType == CELL_CLEAR)) + { + return true; + } + if ((targetType == CELL_CLEAR) && + (srcType == CELL_CLIFF)) + { + return true; + } + return false; +} From 24f354cf8e909d962909000af6db4fd1b0897ba2 Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:22:37 +0200 Subject: [PATCH 08/10] refactor(pathfinder): Move ZoneBlock and global helper functions to their own file (#3429) --- Core/GameEngine/CMakeLists.txt | 1 + .../Include/GameLogic/Pathfinder/ZoneBlock.h | 6 + .../Source/GameLogic/AI/AIPathfind.cpp | 360 ++---------------- .../GameLogic/AI/Pathfinder/ZoneBlock.cpp | 330 ++++++++++++++++ 4 files changed, 369 insertions(+), 328 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/ZoneBlock.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 02959b12691..ee8ee421040 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -869,6 +869,7 @@ set(GAMEENGINE_SRC Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp + Source/GameLogic/AI/Pathfinder/ZoneBlock.cpp # Source/GameLogic/AI/Squad.cpp # Source/GameLogic/AI/TurretAI.cpp Source/GameLogic/Map/PolygonTrigger.cpp diff --git a/Core/GameEngine/Include/GameLogic/Pathfinder/ZoneBlock.h b/Core/GameEngine/Include/GameLogic/Pathfinder/ZoneBlock.h index 87a8e057a9f..59afc8545e0 100644 --- a/Core/GameEngine/Include/GameLogic/Pathfinder/ZoneBlock.h +++ b/Core/GameEngine/Include/GameLogic/Pathfinder/ZoneBlock.h @@ -47,6 +47,12 @@ class ZoneBlock Bool getInteractsWithBridge() const {return m_interactsWithBridge;} void setInteractsWithBridge(Bool interacts) {m_interactsWithBridge = interacts;} + static void __fastcall resolveBlockZones(Int srcZone, Int targetZone, zoneStorageType* zoneEquivalency, Int sizeOfZE); + static void __fastcall resolveZones(Int srcZone, Int targetZone, zoneStorageType* zoneEquivalency, Int sizeOfZE); + static void flattenZones(zoneStorageType* zoneArray, zoneStorageType* zoneHierarchical, Int sizeOfZones); + static void applyZone(PathfindCell& targetCell, const PathfindCell& sourceCell, zoneStorageType* zoneEquivalency, Int sizeOfZE); + static void applyBlockZone(PathfindCell& targetCell, const PathfindCell& sourceCell, zoneStorageType* zoneEquivalency, Int firstZone, Int sizeOfZE); + protected: void allocateZones(); void freeZones(); diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 488bc7cce91..629f9ce2265 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -147,302 +147,6 @@ void Pathfinder::forceCleanCells() } #endif -static void __fastcall resolveBlockZones(Int srcZone, Int targetZone, zoneStorageType *zoneEquivalency, Int sizeOfZE) -{ - Int i; - // We have two zones being combined now. Keep the lower zone. - DEBUG_ASSERTCRASH(srcZone!=0 && targetZone!=0, ("Bad resolve zones .")); - if (targetZone=firstZone && sourceCell.getZone()=firstZone && sourceCell.getZone()getZone(); - if (minZone>zone) minZone=zone; - if (maxZonebounds.lo.x && map[i][j].getZone()!=map[i-1][j].getZone()) { - - if (PathfindCell::waterGround(map[i][j], map[i-1][j])) { - applyBlockZone(map[i][j], map[i-1][j], m_groundWaterZones, m_firstZone, m_numZones); - } - if (PathfindCell::groundRubble(map[i][j], map[i-1][j])) { - applyBlockZone(map[i][j], map[i-1][j], m_groundRubbleZones, m_firstZone, m_numZones); - } - if (PathfindCell::groundCliff(map[i][j], map[i-1][j])) { - applyBlockZone(map[i][j], map[i-1][j], m_groundCliffZones, m_firstZone, m_numZones); - } - if (PathfindCell::crusherGround(map[i][j], map[i-1][j])) { - applyBlockZone(map[i][j], map[i-1][j], m_crusherZones, m_firstZone, m_numZones); - } - } - if (j>bounds.lo.y && map[i][j].getZone()!=map[i][j-1].getZone()) { - if (PathfindCell::waterGround(map[i][j],map[i][j-1])) { - applyBlockZone(map[i][j], map[i][j-1], m_groundWaterZones, m_firstZone, m_numZones); - } - if (PathfindCell::groundRubble(map[i][j], map[i][j-1])) { - applyBlockZone(map[i][j], map[i][j-1], m_groundRubbleZones, m_firstZone, m_numZones); - } - if (PathfindCell::groundCliff(map[i][j],map[i][j-1])) { - applyBlockZone(map[i][j], map[i][j-1], m_groundCliffZones, m_firstZone, m_numZones); - } - if (PathfindCell::crusherGround(map[i][j], map[i][j-1])) { - applyBlockZone(map[i][j], map[i][j-1], m_crusherZones, m_firstZone, m_numZones); - } - } - DEBUG_ASSERTCRASH(map[i][j].getZone() != 0, ("Cleared the zone.")); - } - } - -} - -// -// Return the zone at this location. -// -zoneStorageType ZoneBlock::getEffectiveZone( LocomotorSurfaceTypeMask acceptableSurfaces, - Bool crusher, zoneStorageType zone) const -{ -#if !(RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING) - if (zone==PathfindZoneManager::UNINITIALIZED_ZONE) { - return zone; - } -#endif - - if (acceptableSurfaces&LOCOMOTORSURFACE_AIR) return 1; // air is all zone 1. - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_WATER) && - (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { - // Locomotors can go on ground, water & cliff, so all is zone 1. - return 1; - } - if (m_numZones<2) { - return m_firstZone; // if we only got 1 zone, it's all the same zone. - } - DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); - if (zone= m_firstZone+m_numZones) { - return m_firstZone; - } - zone -= m_firstZone; - if (crusher) { - zone = m_crusherZones[zone]; - DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); - zone -= m_firstZone; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { - // Locomotors can go on ground & cliff, so use the ground cliff combiner. - zone = m_groundCliffZones[zone]; - DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); - return zone; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { - // Locomotors can go on ground & water, so use the ground water combiner. - zone = m_groundWaterZones[zone]; - DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); - return zone; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_RUBBLE)) { - // Locomotors can go on ground & rubble, so use the ground rubble combiner. - zone = m_groundRubbleZones[zone]; - return zone; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF) && - (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { - // Locomotors can go on ground & cliff, so use the ground cliff combiner. - DEBUG_CRASH(("Cliff water only locomotor sets not supported yet.")); - } - - return zone+m_firstZone; -} - - -/* Allocate zone equivalency arrays large enough to hold m_maxZone entries. If the arrays are already -large enough, just return. */ -void ZoneBlock::allocateZones() -{ - if (m_zonesAllocated>m_numZones && m_groundCliffZones!=nullptr) { - return; - } - freeZones(); - - if (m_numZones==1) { - return; // we don't need any zone equivalency tables. - } - - if (m_zonesAllocated == 0) { - m_zonesAllocated = 4; - } - while (m_zonesAllocated <= m_numZones) { - m_zonesAllocated *= 2; - } - // pool[]ify - m_groundCliffZones = MSGNEW("PathfindZoneInfo") zoneStorageType [m_zonesAllocated]; - m_groundWaterZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; - m_groundRubbleZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; - m_crusherZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; -} - - //------------------------ PathfindZoneManager ------------------------------- PathfindZoneManager::PathfindZoneManager() : m_maxZone(0), m_nextFrameToCalculateZones(0), @@ -620,12 +324,12 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye if (i>bounds.lo.x) { if (map[i][j].getType() == map[i-1][j].getType()) { - applyZone(map[i][j], map[i-1][j], zoneEquivalency, m_maxZone); + ZoneBlock::applyZone(map[i][j], map[i-1][j], zoneEquivalency, m_maxZone); } } if (j>bounds.lo.y) { if (map[i][j].getType() == map[i][j-1].getType()) { - applyZone(map[i][j], map[i][j-1], zoneEquivalency, m_maxZone); + ZoneBlock::applyZone(map[i][j], map[i][j - 1], zoneEquivalency, m_maxZone); } } if (cell->getZone()==0) { @@ -730,7 +434,7 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye if ( (r_thisCell.getConnectLayer() > LAYER_GROUND) && (r_thisCell.getType() == PathfindCell::CELL_CLEAR) ) { PathfindLayer *layer = layers + r_thisCell.getConnectLayer(); - resolveZones(r_thisCell.getZone(), layer->getZone(), m_hierarchicalZones, m_maxZone); + ZoneBlock::resolveZones(r_thisCell.getZone(), layer->getZone(), m_hierarchicalZones, m_maxZone); } if ( i > globalBounds.lo.x && r_thisCell.getZone() != map[i-1][j].getZone() ) { @@ -738,47 +442,47 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye #if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING if (r_thisCell.getType() == r_leftCell.getType()) { - applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); } if (PathfindCell::waterGround(r_thisCell, r_leftCell)) { - applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); } if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) { - applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); } if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) { - applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); } if (PathfindCell::terrain(r_thisCell, r_leftCell)) { - applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); } if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { - applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); } #else //if this is true, skip all the ones below if (r_thisCell.getType() == r_leftCell.getType()) - applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); else { Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below if (PathfindCell::terrain(r_thisCell, r_leftCell)) { - applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); notTerrainOrCrusher = FALSE; } if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { - applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); notTerrainOrCrusher = FALSE; } if ( notTerrainOrCrusher ) { if (PathfindCell::waterGround(r_thisCell, r_leftCell)) - applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); else if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) - applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); else if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) - applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); } } @@ -791,47 +495,47 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye #if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING if (r_thisCell.getType() == r_topCell.getType()) { - applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); } if (PathfindCell::waterGround(r_thisCell, r_topCell)) { - applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); } if (PathfindCell::groundRubble(r_thisCell, r_topCell)) { - applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); } if (PathfindCell::groundCliff(r_thisCell, r_topCell)) { - applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); } if (PathfindCell::terrain(r_thisCell, r_topCell)) { - applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); } if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { - applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); } #else //if this is true, skip all the ones below if (r_thisCell.getType() == r_topCell.getType()) - applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); else { Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below if (PathfindCell::terrain(r_thisCell, r_topCell)) { - applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); notTerrainOrCrusher = FALSE; } if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { - applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); notTerrainOrCrusher = FALSE; } if (notTerrainOrCrusher) { if (PathfindCell::waterGround(r_thisCell, r_topCell)) - applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); else if (PathfindCell::groundRubble(r_thisCell, r_topCell)) - applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); else if (PathfindCell::groundCliff(r_thisCell, r_topCell)) - applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); } } @@ -849,11 +553,11 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye } //THIS BLOCK IS 20% - flattenZones(m_groundCliffZones, m_hierarchicalZones, m_maxZone); - flattenZones(m_groundWaterZones, m_hierarchicalZones, m_maxZone); - flattenZones(m_groundRubbleZones, m_hierarchicalZones, m_maxZone); - flattenZones(m_terrainZones, m_hierarchicalZones, m_maxZone); - flattenZones(m_crusherZones, m_hierarchicalZones, m_maxZone); + ZoneBlock::flattenZones(m_groundCliffZones, m_hierarchicalZones, m_maxZone); + ZoneBlock::flattenZones(m_groundWaterZones, m_hierarchicalZones, m_maxZone); + ZoneBlock::flattenZones(m_groundRubbleZones, m_hierarchicalZones, m_maxZone); + ZoneBlock::flattenZones(m_terrainZones, m_hierarchicalZones, m_maxZone); + ZoneBlock::flattenZones(m_crusherZones, m_hierarchicalZones, m_maxZone); #ifdef DEBUG_QPF #if defined(DEBUG_LOGGING) diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/ZoneBlock.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/ZoneBlock.cpp new file mode 100644 index 00000000000..7777bfd96c2 --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/ZoneBlock.cpp @@ -0,0 +1,330 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/Pathfinder/PathfindCell.h" +#include "GameLogic/Pathfinder/PathfindZoneManager.h" +#include "GameLogic/Pathfinder/ZoneBlock.h" + +ZoneBlock::ZoneBlock() : m_firstZone(0), +m_numZones(0), +m_groundCliffZones(nullptr), +m_groundWaterZones(nullptr), +m_groundRubbleZones(nullptr), +m_crusherZones(nullptr), +m_zonesAllocated(0), +m_interactsWithBridge(FALSE) +{ + m_cellOrigin.x = 0; + m_cellOrigin.y = 0; + m_firstZone = 0; + m_markedPassable = TRUE; +} + +ZoneBlock::~ZoneBlock() +{ + freeZones(); +} + +void ZoneBlock::freeZones() +{ + delete [] m_groundCliffZones; + m_groundCliffZones = nullptr; + + delete [] m_groundWaterZones; + m_groundWaterZones = nullptr; + + delete [] m_groundRubbleZones; + m_groundRubbleZones = nullptr; + + delete [] m_crusherZones; + m_crusherZones = nullptr; +} + +/* Allocate zone equivalency arrays large enough to hold required entries. If the arrays are already +large enough, reuse. Then calculate terrain equivalencies. */ +void ZoneBlock::blockCalculateZones(PathfindCell **map, PathfindLayer layers[], const IRegion2D &bounds) +{ + Int i, j; + m_cellOrigin = bounds.lo; + UnsignedInt minZone = map[bounds.lo.x][bounds.lo.y].getZone(); + UnsignedInt maxZone = minZone; + + for( j=bounds.lo.y; j<=bounds.hi.y; j++ ) { + for( i=bounds.lo.x; i<=bounds.hi.x; i++ ) { + PathfindCell *cell = &map[i][j]; + zoneStorageType zone = cell->getZone(); + if (minZone>zone) minZone=zone; + if (maxZonebounds.lo.x && map[i][j].getZone()!=map[i-1][j].getZone()) { + + if (PathfindCell::waterGround(map[i][j], map[i-1][j])) { + applyBlockZone(map[i][j], map[i-1][j], m_groundWaterZones, m_firstZone, m_numZones); + } + if (PathfindCell::groundRubble(map[i][j], map[i-1][j])) { + applyBlockZone(map[i][j], map[i-1][j], m_groundRubbleZones, m_firstZone, m_numZones); + } + if (PathfindCell::groundCliff(map[i][j], map[i-1][j])) { + applyBlockZone(map[i][j], map[i-1][j], m_groundCliffZones, m_firstZone, m_numZones); + } + if (PathfindCell::crusherGround(map[i][j], map[i-1][j])) { + applyBlockZone(map[i][j], map[i-1][j], m_crusherZones, m_firstZone, m_numZones); + } + } + if (j>bounds.lo.y && map[i][j].getZone()!=map[i][j-1].getZone()) { + if (PathfindCell::waterGround(map[i][j],map[i][j-1])) { + applyBlockZone(map[i][j], map[i][j-1], m_groundWaterZones, m_firstZone, m_numZones); + } + if (PathfindCell::groundRubble(map[i][j], map[i][j-1])) { + applyBlockZone(map[i][j], map[i][j-1], m_groundRubbleZones, m_firstZone, m_numZones); + } + if (PathfindCell::groundCliff(map[i][j],map[i][j-1])) { + applyBlockZone(map[i][j], map[i][j-1], m_groundCliffZones, m_firstZone, m_numZones); + } + if (PathfindCell::crusherGround(map[i][j], map[i][j-1])) { + applyBlockZone(map[i][j], map[i][j-1], m_crusherZones, m_firstZone, m_numZones); + } + } + DEBUG_ASSERTCRASH(map[i][j].getZone() != 0, ("Cleared the zone.")); + } + } + +} + +// +// Return the zone at this location. +// +zoneStorageType ZoneBlock::getEffectiveZone( LocomotorSurfaceTypeMask acceptableSurfaces, + Bool crusher, zoneStorageType zone) const +{ +#if !(RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING) + if (zone==PathfindZoneManager::UNINITIALIZED_ZONE) { + return zone; + } +#endif + + if (acceptableSurfaces&LOCOMOTORSURFACE_AIR) return 1; // air is all zone 1. + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_WATER) && + (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { + // Locomotors can go on ground, water & cliff, so all is zone 1. + return 1; + } + if (m_numZones<2) { + return m_firstZone; // if we only got 1 zone, it's all the same zone. + } + DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); + if (zone= m_firstZone+m_numZones) { + return m_firstZone; + } + zone -= m_firstZone; + if (crusher) { + zone = m_crusherZones[zone]; + DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); + zone -= m_firstZone; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { + // Locomotors can go on ground & cliff, so use the ground cliff combiner. + zone = m_groundCliffZones[zone]; + DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); + return zone; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { + // Locomotors can go on ground & water, so use the ground water combiner. + zone = m_groundWaterZones[zone]; + DEBUG_ASSERTCRASH(zone >=m_firstZone && zone < m_firstZone+m_numZones, ("Invalid range.")); + return zone; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_RUBBLE)) { + // Locomotors can go on ground & rubble, so use the ground rubble combiner. + zone = m_groundRubbleZones[zone]; + return zone; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF) && + (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { + // Locomotors can go on ground & cliff, so use the ground cliff combiner. + DEBUG_CRASH(("Cliff water only locomotor sets not supported yet.")); + } + + return zone+m_firstZone; +} + + +/* Allocate zone equivalency arrays large enough to hold m_maxZone entries. If the arrays are already +large enough, just return. */ +void ZoneBlock::allocateZones() +{ + if (m_zonesAllocated>m_numZones && m_groundCliffZones!=nullptr) { + return; + } + freeZones(); + + if (m_numZones==1) { + return; // we don't need any zone equivalency tables. + } + + if (m_zonesAllocated == 0) { + m_zonesAllocated = 4; + } + while (m_zonesAllocated <= m_numZones) { + m_zonesAllocated *= 2; + } + // pool[]ify + m_groundCliffZones = MSGNEW("PathfindZoneInfo") zoneStorageType [m_zonesAllocated]; + m_groundWaterZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; + m_groundRubbleZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; + m_crusherZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; +} + +void __fastcall ZoneBlock::resolveBlockZones(Int srcZone, Int targetZone, zoneStorageType* zoneEquivalency, Int sizeOfZE) +{ + Int i; + // We have two zones being combined now. Keep the lower zone. + DEBUG_ASSERTCRASH(srcZone != 0 && targetZone != 0, ("Bad resolve zones .")); + if (targetZone < srcZone) + { + for (i = 0; i < sizeOfZE; i++) + { + if (zoneEquivalency[i] == srcZone) + { + zoneEquivalency[i] = targetZone; + } + } + } + else + { + for (i = 0; i < sizeOfZE; i++) + { + if (zoneEquivalency[i] == targetZone) + { + zoneEquivalency[i] = srcZone; + } + } + } +} + +void __fastcall ZoneBlock::resolveZones(Int srcZone, Int targetZone, zoneStorageType* zoneEquivalency, Int sizeOfZE) +{ + Int i; + // We have two zones being combined now. Keep the lower zone. + DEBUG_ASSERTCRASH(srcZone != 0 && targetZone != 0, ("Bad resolve zones .")); + DEBUG_ASSERTCRASH(srcZone < sizeOfZE && targetZone < sizeOfZE, ("Bad resolve zones .")); + srcZone = zoneEquivalency[srcZone]; + targetZone = zoneEquivalency[targetZone]; + DEBUG_ASSERTCRASH(srcZone < sizeOfZE && targetZone < sizeOfZE, ("Bad resolve zones .")); + zoneStorageType finalZone; + if (targetZone < srcZone) + { + finalZone = zoneEquivalency[targetZone]; + } + else + { + finalZone = zoneEquivalency[srcZone]; + } + DEBUG_ASSERTCRASH(finalZone < sizeOfZE, ("Bad resolve zones .")); + for (i = 0; i < sizeOfZE; i++) + { + zoneStorageType ze = zoneEquivalency[i]; + if (ze == targetZone || ze == srcZone) + { + zoneEquivalency[i] = finalZone; + } + } +} + +void ZoneBlock::flattenZones(zoneStorageType* zoneArray, zoneStorageType* zoneHierarchical, Int sizeOfZones) +{ + Int i; + for (i = 0; i < sizeOfZones; i++) + { + Int zone1 = zoneArray[i]; + Int zone2 = zoneHierarchical[zone1]; + zone1 = zoneArray[zone2]; + zone2 = zoneHierarchical[zone1]; + zoneArray[i] = zone2; + } +#if 1 + + for (i = 0; i < sizeOfZones; i++) + { + Int zone1 = zoneArray[i]; + Int zone2 = zoneHierarchical[i]; + if (zone1 != zone2) + { + resolveZones(zone1, zone2, zoneArray, sizeOfZones); + } + } +#endif +} + +void ZoneBlock::applyZone(PathfindCell& targetCell, const PathfindCell& sourceCell, zoneStorageType* zoneEquivalency, Int sizeOfZE) +{ + DEBUG_ASSERTCRASH(sourceCell.getZone() != 0, ("Unset source zone.")); + Int srcZone = zoneEquivalency[sourceCell.getZone()]; + Int targetZone = zoneEquivalency[targetCell.getZone()]; + + if (targetZone == 0) + { + targetCell.setZone(srcZone); + return; + } + if (targetZone == srcZone) + { + return; // already match. + } + resolveZones(srcZone, targetZone, zoneEquivalency, sizeOfZE); +} + +void ZoneBlock::applyBlockZone(PathfindCell& targetCell, const PathfindCell& sourceCell, zoneStorageType* zoneEquivalency, Int firstZone, Int sizeOfZE) +{ + DEBUG_ASSERTCRASH(sourceCell.getZone() >= firstZone && sourceCell.getZone() < firstZone + sizeOfZE, ("Memory overrun - FATAL ERROR.")); + Int srcZone = zoneEquivalency[sourceCell.getZone() - firstZone]; + DEBUG_ASSERTCRASH(targetCell.getZone() >= firstZone && sourceCell.getZone() < firstZone + sizeOfZE, ("Memory overrun - FATAL ERROR.")); + Int targetZone = zoneEquivalency[targetCell.getZone() - firstZone]; + if (targetZone == srcZone) + { + return; // already match. + } + resolveBlockZones(srcZone, targetZone, zoneEquivalency, sizeOfZE); +} From 67356f210373da70f2860698406c143aae7c961a Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:37:44 +0200 Subject: [PATCH 09/10] refactor(pathfinder): Move PathfindZoneManager to its own file (#3429) --- Core/GameEngine/CMakeLists.txt | 1 + .../Source/GameLogic/AI/AIPathfind.cpp | 800 ----------------- .../AI/Pathfinder/PathfindZoneManager.cpp | 824 ++++++++++++++++++ 3 files changed, 825 insertions(+), 800 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindZoneManager.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index ee8ee421040..4e4dd4fbdce 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -868,6 +868,7 @@ set(GAMEENGINE_SRC Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp + Source/GameLogic/AI/Pathfinder/PathfindZoneManager.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp Source/GameLogic/AI/Pathfinder/ZoneBlock.cpp # Source/GameLogic/AI/Squad.cpp diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 629f9ce2265..92b2721d11f 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -147,806 +147,6 @@ void Pathfinder::forceCleanCells() } #endif -//------------------------ PathfindZoneManager ------------------------------- -PathfindZoneManager::PathfindZoneManager() : m_maxZone(0), -m_nextFrameToCalculateZones(0), -m_groundCliffZones(nullptr), -m_groundWaterZones(nullptr), -m_groundRubbleZones(nullptr), -m_terrainZones(nullptr), -m_crusherZones(nullptr), -m_hierarchicalZones(nullptr), -m_blockOfZoneBlocks(nullptr), -m_zoneBlocks(nullptr), -m_zonesAllocated(0) -{ - m_zoneBlockExtent.x = 0; - m_zoneBlockExtent.y = 0; -} - -PathfindZoneManager::~PathfindZoneManager() -{ - freeZones(); - freeBlocks(); -} - -void PathfindZoneManager::freeZones() -{ - delete [] m_groundCliffZones; - m_groundCliffZones = nullptr; - - delete [] m_groundWaterZones; - m_groundWaterZones = nullptr; - - delete [] m_groundRubbleZones; - m_groundRubbleZones = nullptr; - - delete [] m_terrainZones; - m_terrainZones = nullptr; - - delete [] m_crusherZones; - m_crusherZones = nullptr; - - delete [] m_hierarchicalZones; - m_hierarchicalZones = nullptr; - - m_zonesAllocated = 0; -} - -void PathfindZoneManager::freeBlocks() -{ - delete [] m_blockOfZoneBlocks; - m_blockOfZoneBlocks = nullptr; - - delete [] m_zoneBlocks; - m_zoneBlocks = nullptr; - - m_zoneBlockExtent.x = 0; - m_zoneBlockExtent.y = 0; -} - -/* Allocate zone equivalency arrays large enough to hold m_maxZone entries. If the arrays are already -large enough, just return. */ -void PathfindZoneManager::allocateZones() -{ - if (m_zonesAllocated>m_maxZone && m_groundCliffZones!=nullptr) { - return; - } - freeZones(); - - if (m_zonesAllocated == 0) { - m_zonesAllocated = INITIAL_ZONES; - } - while (m_zonesAllocated <= m_maxZone) { - m_zonesAllocated *= 2; - } - DEBUG_LOG(("Allocating zone tables of size %d", m_zonesAllocated)); - // pool[]ify - m_groundCliffZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; - m_groundWaterZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; - m_groundRubbleZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; - m_terrainZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; - m_crusherZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; - m_hierarchicalZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; -} - -/* Allocate zone blocks for hierarchical pathfinding. */ -void PathfindZoneManager::allocateBlocks(const IRegion2D &globalBounds) -{ - freeBlocks(); - - m_zoneBlockExtent.x = (globalBounds.hi.x-globalBounds.lo.x+1+ZONE_BLOCK_SIZE-1)/ZONE_BLOCK_SIZE; - m_zoneBlockExtent.y = (globalBounds.hi.y-globalBounds.lo.y+1+ZONE_BLOCK_SIZE-1)/ZONE_BLOCK_SIZE; - - m_blockOfZoneBlocks = MSGNEW("PathfindZoneBlocks") ZoneBlock[(m_zoneBlockExtent.x)*(m_zoneBlockExtent.y)]; - m_zoneBlocks = MSGNEW("PathfindZoneBlocks") ZoneBlockP[m_zoneBlockExtent.x]; - Int i; - for (i=0; igetFrame(); -#else - if (TheGameLogic->getFrame()<2) { - m_nextFrameToCalculateZones = 2; - return; - } - m_nextFrameToCalculateZones = MIN( m_nextFrameToCalculateZones, TheGameLogic->getFrame() + ZONE_UPDATE_FREQUENCY ); -#endif -} - -/** - * Calculate zones. A zone is an area of the same terrain - clear, water or cliff. - * The utility of zones is that if current location and destination are in the same zone, - * you can successfully pathfind. - * If you are a multiple terrain vehicle, like amphibious transport, the lookup is a little more - * complicated. - */ -void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer layers[], const IRegion2D &globalBounds ) -{ -#ifdef DEBUG_QPF -#if defined(DEBUG_LOGGING) - __int64 startTime64; - static double timeToUpdate = 0.0f; - static double averageTimeToUpdate = 0.0f; - static Int updateSamples = 0; - __int64 endTime64,freq64; - QueryPerformanceFrequency((LARGE_INTEGER *)&freq64); - QueryPerformanceCounter((LARGE_INTEGER *)&startTime64); -#endif -#endif - - m_maxZone = 1; // we start using zone 0 as a flag. - const Int maxZones=24000; - zoneStorageType zoneEquivalency[maxZones]; - Int i, j; - for (i=0; ibounds.hi.x || bounds.lo.y>bounds.hi.y) { - DEBUG_CRASH(("Incorrect bounds calculation. Logic error, fix me. jba.")); - continue; - } -#endif - m_zoneBlocks[xBlock][yBlock].setInteractsWithBridge(false); - for( j=bounds.lo.y; j<=bounds.hi.y; j++ ) { - for( i=bounds.lo.x; i<=bounds.hi.x; i++ ) { - PathfindCell *cell = &map[i][j]; - cell->setZone(0); - - if (i>bounds.lo.x) { - if (map[i][j].getType() == map[i-1][j].getType()) { - ZoneBlock::applyZone(map[i][j], map[i-1][j], zoneEquivalency, m_maxZone); - } - } - if (j>bounds.lo.y) { - if (map[i][j].getType() == map[i][j-1].getType()) { - ZoneBlock::applyZone(map[i][j], map[i][j - 1], zoneEquivalency, m_maxZone); - } - } - if (cell->getZone()==0) { - cell->setZone(m_maxZone); - m_maxZone++; -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - if (m_maxZone>= maxZones) { - DEBUG_CRASH(("Ran out of pathfind zones. SERIOUS ERROR! jba.")); - break; - } -#endif - } - if (cell->getConnectLayer() > LAYER_GROUND) { - m_zoneBlocks[xBlock][yBlock].setInteractsWithBridge(true); - } - - } - } - } - } - - Int totalZones = m_maxZone; - - // Collapse the zones into a 1,2,3... sequence, removing collapsed zones. - m_maxZone = 1; - Int collapsedZones[maxZones]; - collapsedZones[0] = 0; - for (i=1; ibounds.hi.x || bounds.lo.y>bounds.hi.y) { - DEBUG_CRASH(("Incorrect bounds calculation. Logic error, fix me. jba.")); - continue; - } -#endif - m_zoneBlocks[xBlock][yBlock].blockCalculateZones(map, layers, bounds); - } - } - - // Determine water/ground equivalent zones, and ground/cliff equivalent zones. - for (i=0; i LAYER_GROUND) && - (r_thisCell.getType() == PathfindCell::CELL_CLEAR) ) { - PathfindLayer *layer = layers + r_thisCell.getConnectLayer(); - ZoneBlock::resolveZones(r_thisCell.getZone(), layer->getZone(), m_hierarchicalZones, m_maxZone); - } - - if ( i > globalBounds.lo.x && r_thisCell.getZone() != map[i-1][j].getZone() ) { - const PathfindCell &r_leftCell = map[i-1][j]; - -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - if (r_thisCell.getType() == r_leftCell.getType()) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); - } - if (PathfindCell::waterGround(r_thisCell, r_leftCell)) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); - } - if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); - } - if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); - } - if (PathfindCell::terrain(r_thisCell, r_leftCell)) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); - } - if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); - } -#else - //if this is true, skip all the ones below - if (r_thisCell.getType() == r_leftCell.getType()) - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); - else { - Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below - - if (PathfindCell::terrain(r_thisCell, r_leftCell)) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); - notTerrainOrCrusher = FALSE; - } - - if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); - notTerrainOrCrusher = FALSE; - } - - if ( notTerrainOrCrusher ) { - if (PathfindCell::waterGround(r_thisCell, r_leftCell)) - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); - else if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); - else if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) - ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); - } - - } -#endif - - } - - if (j>globalBounds.lo.y && r_thisCell.getZone()!=map[i][j-1].getZone()) { - const PathfindCell &r_topCell = map[i][j-1]; - -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - if (r_thisCell.getType() == r_topCell.getType()) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); - } - if (PathfindCell::waterGround(r_thisCell, r_topCell)) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); - } - if (PathfindCell::groundRubble(r_thisCell, r_topCell)) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); - } - if (PathfindCell::groundCliff(r_thisCell, r_topCell)) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); - } - if (PathfindCell::terrain(r_thisCell, r_topCell)) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); - } - if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); - } -#else - //if this is true, skip all the ones below - if (r_thisCell.getType() == r_topCell.getType()) - ZoneBlock::applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); - else { - Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below - - if (PathfindCell::terrain(r_thisCell, r_topCell)) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); - notTerrainOrCrusher = FALSE; - } - - if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { - ZoneBlock::applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); - notTerrainOrCrusher = FALSE; - } - - if (notTerrainOrCrusher) { - if (PathfindCell::waterGround(r_thisCell, r_topCell)) - ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); - else if (PathfindCell::groundRubble(r_thisCell, r_topCell)) - ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); - else if (PathfindCell::groundCliff(r_thisCell, r_topCell)) - ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); - } - - } -#endif - - } - - } - } - - //FLATTEN HIERARCHICAL ZONES - for (i=1; im_debugAI == AI_DEBUG_ZONES) - { - extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); - RGBColor color; - memset(&color, 0, sizeof(Color)); - addIcon(nullptr, 0, 0, color); - for( j=0; jgetLayerHeight( pos.x, pos.y, map[i][j].getLayer() ) + 0.5f; - addIcon(&pos, PATHFIND_CELL_SIZE_F*0.8f, 500, color); - } - } - } -#endif - m_nextFrameToCalculateZones = 0xffffffff; -} - -/** - * Update zones where a structure has been added or removed. - * This can be done by just updating the equivalency arrays, without rezoning the map.. - */ -void PathfindZoneManager::updateZonesForModify(PathfindCell **map, PathfindLayer layers[], const IRegion2D &structureBounds, const IRegion2D &globalBounds ) -{ - -#ifdef DEBUG_QPF -#if defined(DEBUG_LOGGING) - __int64 startTime64; - double timeToUpdate=0.0f; - __int64 endTime64,freq64; - QueryPerformanceFrequency((LARGE_INTEGER *)&freq64); - QueryPerformanceCounter((LARGE_INTEGER *)&startTime64); -#endif -#endif - IRegion2D bounds = structureBounds; - bounds.hi.x++; - bounds.hi.y++; - bounds.hi.updateMin(globalBounds.hi); - - Int xBlock, yBlock; - for (xBlock = 0; xBlockblockBounds.hi.x || blockBounds.lo.y>blockBounds.hi.y) { - continue; - } - m_zoneBlocks[xBlock][yBlock].setInteractsWithBridge(false); - Int i, j; - for( j=blockBounds.lo.y; j<=blockBounds.hi.y; j++ ) { - for( i=blockBounds.lo.x; i<=blockBounds.hi.x; i++ ) { - PathfindCell *cell = &map[i][j]; - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - - if (i>blockBounds.lo.x) { - if (map[i][j].getType() == map[i-1][j].getType()) { - cell->setZone(map[i-1][j].getZone()); - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - } - } - if (j>blockBounds.lo.y) { - if (cell->getType() == map[i][j-1].getType()) { - cell->setZone(map[i][j-1].getZone()); - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - } - if (isetZone(map[i+1][j-1].getZone()); - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - } - } - } - } - } - for( j=blockBounds.hi.y; j>=blockBounds.lo.y; j-- ) { - for( i=blockBounds.hi.x; i>=blockBounds.lo.x; i-- ) { - PathfindCell *cell = &map[i][j]; - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - if (isetZone(map[i+1][j].getZone()); - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - } - } - if (jgetType() == map[i][j+1].getType()) { - cell->setZone(map[i][j+1].getZone()); - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - } - if (isetZone(map[i+1][j+1].getZone()); - if (cell->getZone()!=UNINITIALIZED_ZONE) continue; - } - } - } - } - } - } - } -#ifdef DEBUG_QPF -#if defined(DEBUG_LOGGING) - QueryPerformanceCounter((LARGE_INTEGER *)&endTime64); - timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64)); -#endif -#endif -#if defined(RTS_DEBUG) - if (TheGlobalData->m_debugAI==AI_DEBUG_ZONES) - { - extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); - RGBColor color; - memset(&color, 0, sizeof(Color)); - addIcon(nullptr, 0, 0, color); - Int i, j; - for( j=0; jgetLayerHeight( pos.x, pos.y, map[i][j].getLayer() ) + 0.5f; - addIcon(&pos, PATHFIND_CELL_SIZE_F*0.8f, 200, color); - } - } - } -#endif - -} - -// -// Clear the passable flags. -// -void PathfindZoneManager::clearPassableFlags() -{ Int blockX; - Int blockY; - for (blockX = 0; blockX=m_zoneBlockExtent.x) { - DEBUG_CRASH(("Invalid block.")); - return; - } - if (blockY<0 || blockY>=m_zoneBlockExtent.y) { - DEBUG_CRASH(("Invalid block.")); - return; - } - m_zoneBlocks[blockX][blockY].setPassable(passable); -} - -// -// Get the passable flag for the block at this location. -// -Bool PathfindZoneManager::isPassable(Int cellX, Int cellY) const -{ - Int blockX = cellX/ZONE_BLOCK_SIZE; - Int blockY = cellY/ZONE_BLOCK_SIZE; - - if (blockX<0 || blockX>=m_zoneBlockExtent.x) { - DEBUG_CRASH(("Invalid block.")); - return false; - } - if (blockY<0 || blockY>=m_zoneBlockExtent.y) { - DEBUG_CRASH(("Invalid block.")); - return false; - } - return m_zoneBlocks[blockX][blockY].isPassable(); -} - -// -// Get the passable flag for the block at this location. -// -Bool PathfindZoneManager::clipIsPassable(Int cellX, Int cellY) const -{ - Int blockX = cellX/ZONE_BLOCK_SIZE; - Int blockY = cellY/ZONE_BLOCK_SIZE; - - if (blockX<0 || blockX>=m_zoneBlockExtent.x) { - return false; - } - if (blockY<0 || blockY>=m_zoneBlockExtent.y) { - return false; - } - return m_zoneBlocks[blockX][blockY].isPassable(); -} - -// -// Set the bridge flag for the block at this location. -// -void PathfindZoneManager::setBridge(Int cellX, Int cellY, Bool bridge) -{ - Int blockX = cellX/ZONE_BLOCK_SIZE; - Int blockY = cellY/ZONE_BLOCK_SIZE; - - if (blockX<0 || blockX>=m_zoneBlockExtent.x) { - // DEBUG_CRASH(("Invalid block.")); Bridges can be off the playable grid, so don't crash. jba. - return; - } - if (blockY<0 || blockY>=m_zoneBlockExtent.y) { - // DEBUG_CRASH(("Invalid block.")); Bridges can be off the playable grid, so don't crash. jba. - return; - } - m_zoneBlocks[blockX][blockY].setInteractsWithBridge(bridge); -} - - -// -// Set the bridge flag for the block at this location. -// -Bool PathfindZoneManager::interactsWithBridge(Int cellX, Int cellY) const -{ - Int blockX = cellX/ZONE_BLOCK_SIZE; - Int blockY = cellY/ZONE_BLOCK_SIZE; - - if (blockX<0 || blockX>=m_zoneBlockExtent.x) { - DEBUG_CRASH(("Invalid block.")); - return false; - } - if (blockY<0 || blockY>=m_zoneBlockExtent.y) { - DEBUG_CRASH(("Invalid block.")); - return false; - } - return m_zoneBlocks[blockX][blockY].getInteractsWithBridge(); -} - - -// -// Return the zone at this location. -// -zoneStorageType PathfindZoneManager::getBlockZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher,Int cellX, Int cellY, PathfindCell **map) const -{ - PathfindCell *cell = &(map[cellX][cellY]); - Int blockX = cellX/ZONE_BLOCK_SIZE; - Int blockY = cellY/ZONE_BLOCK_SIZE; - - if (blockX<0 || blockX>=m_zoneBlockExtent.x) { - DEBUG_CRASH(("Invalid block.")); - return 0; - } - if (blockY<0 || blockY>=m_zoneBlockExtent.y) { - DEBUG_CRASH(("Invalid block.")); - return 0; - } - zoneStorageType zone = m_zoneBlocks[blockX][blockY].getEffectiveZone(acceptableSurfaces, crusher, cell->getZone()); -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - if (zone > m_maxZone) { -#else - if (zone >= m_maxZone) { -#endif - DEBUG_CRASH(("Invalid zone.")); - return UNINITIALIZED_ZONE; - } - return zone; -} - -// -// Return the zone at this location. -// -zoneStorageType PathfindZoneManager::getEffectiveTerrainZone(zoneStorageType zone) const -{ - return m_hierarchicalZones[m_terrainZones[zone]]; -} - -// -// Return the zone at this location. -// -zoneStorageType PathfindZoneManager::getEffectiveZone( LocomotorSurfaceTypeMask acceptableSurfaces, - Bool crusher, zoneStorageType zone) const -{ - //DEBUG_ASSERTCRASH(zone, ("Zone not set")); - if (zone>m_maxZone) { - DEBUG_CRASH(("Invalid zone")); - return (0); - } - if (zone>m_maxZone) { - DEBUG_CRASH(("Invalid zone")); - return (0); - } - if (acceptableSurfaces&LOCOMOTORSURFACE_AIR) return 1; // air is all zone 1. - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_WATER) && - (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { - // Locomotors can go on ground, water & cliff, so all is zone 1. - return 1; - } - - if (crusher) { - zone = m_crusherZones[zone]; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { - // Locomotors can go on ground & cliff, so use the ground cliff combiner. - zone = m_groundCliffZones[zone]; - return zone; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { - // Locomotors can go on ground & water, so use the ground water combiner. - zone = m_groundWaterZones[zone]; - return zone; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && - (acceptableSurfaces&LOCOMOTORSURFACE_RUBBLE)) { - // Locomotors can go on ground & rubble, so use the ground rubble combiner. - zone = m_groundRubbleZones[zone]; - return zone; - } - - if ( (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF) && - (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { - // Locomotors can go on ground & cliff, so use the ground cliff combiner. - DEBUG_CRASH(("Cliff water only locomotor sets not supported yet.")); - } - zone = m_hierarchicalZones[zone]; - - return zone; -} //-------------------- PathfindLayer ---------------------------------------- PathfindLayer::PathfindLayer() : m_blockOfMapCells(nullptr), m_layerCells(nullptr), m_bridge(nullptr), m_destroyed(FALSE), diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindZoneManager.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindZoneManager.cpp new file mode 100644 index 00000000000..82c2e4ef306 --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindZoneManager.cpp @@ -0,0 +1,824 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/Pathfinder/PathfindCell.h" +#include "GameLogic/Pathfinder/PathfindConstants.h" +#include "GameLogic/Pathfinder/PathfindLayer.h" +#include "GameLogic/Pathfinder/PathfindZoneManager.h" +#include "GameLogic/Pathfinder/ZoneBlock.h" +#include "GameLogic/TerrainLogic.h" + +PathfindZoneManager::PathfindZoneManager() : m_maxZone(0), +m_nextFrameToCalculateZones(0), +m_groundCliffZones(nullptr), +m_groundWaterZones(nullptr), +m_groundRubbleZones(nullptr), +m_terrainZones(nullptr), +m_crusherZones(nullptr), +m_hierarchicalZones(nullptr), +m_blockOfZoneBlocks(nullptr), +m_zoneBlocks(nullptr), +m_zonesAllocated(0) +{ + m_zoneBlockExtent.x = 0; + m_zoneBlockExtent.y = 0; +} + +PathfindZoneManager::~PathfindZoneManager() +{ + freeZones(); + freeBlocks(); +} + +void PathfindZoneManager::freeZones() +{ + delete [] m_groundCliffZones; + m_groundCliffZones = nullptr; + + delete [] m_groundWaterZones; + m_groundWaterZones = nullptr; + + delete [] m_groundRubbleZones; + m_groundRubbleZones = nullptr; + + delete [] m_terrainZones; + m_terrainZones = nullptr; + + delete [] m_crusherZones; + m_crusherZones = nullptr; + + delete [] m_hierarchicalZones; + m_hierarchicalZones = nullptr; + + m_zonesAllocated = 0; +} + +void PathfindZoneManager::freeBlocks() +{ + delete [] m_blockOfZoneBlocks; + m_blockOfZoneBlocks = nullptr; + + delete [] m_zoneBlocks; + m_zoneBlocks = nullptr; + + m_zoneBlockExtent.x = 0; + m_zoneBlockExtent.y = 0; +} + +/* Allocate zone equivalency arrays large enough to hold m_maxZone entries. If the arrays are already +large enough, just return. */ +void PathfindZoneManager::allocateZones() +{ + if (m_zonesAllocated>m_maxZone && m_groundCliffZones!=nullptr) { + return; + } + freeZones(); + + if (m_zonesAllocated == 0) { + m_zonesAllocated = INITIAL_ZONES; + } + while (m_zonesAllocated <= m_maxZone) { + m_zonesAllocated *= 2; + } + DEBUG_LOG(("Allocating zone tables of size %d", m_zonesAllocated)); + // pool[]ify + m_groundCliffZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; + m_groundWaterZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; + m_groundRubbleZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; + m_terrainZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; + m_crusherZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; + m_hierarchicalZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; +} + +/* Allocate zone blocks for hierarchical pathfinding. */ +void PathfindZoneManager::allocateBlocks(const IRegion2D &globalBounds) +{ + freeBlocks(); + + m_zoneBlockExtent.x = (globalBounds.hi.x-globalBounds.lo.x+1+ZONE_BLOCK_SIZE-1)/ZONE_BLOCK_SIZE; + m_zoneBlockExtent.y = (globalBounds.hi.y-globalBounds.lo.y+1+ZONE_BLOCK_SIZE-1)/ZONE_BLOCK_SIZE; + + m_blockOfZoneBlocks = MSGNEW("PathfindZoneBlocks") ZoneBlock[(m_zoneBlockExtent.x)*(m_zoneBlockExtent.y)]; + m_zoneBlocks = MSGNEW("PathfindZoneBlocks") ZoneBlockP[m_zoneBlockExtent.x]; + Int i; + for (i=0; igetFrame(); +#else + if (TheGameLogic->getFrame()<2) { + m_nextFrameToCalculateZones = 2; + return; + } + m_nextFrameToCalculateZones = MIN( m_nextFrameToCalculateZones, TheGameLogic->getFrame() + ZONE_UPDATE_FREQUENCY ); +#endif +} + +/** + * Calculate zones. A zone is an area of the same terrain - clear, water or cliff. + * The utility of zones is that if current location and destination are in the same zone, + * you can successfully pathfind. + * If you are a multiple terrain vehicle, like amphibious transport, the lookup is a little more + * complicated. + */ +void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer layers[], const IRegion2D &globalBounds ) +{ +#ifdef DEBUG_QPF +#if defined(DEBUG_LOGGING) + __int64 startTime64; + static double timeToUpdate = 0.0f; + static double averageTimeToUpdate = 0.0f; + static Int updateSamples = 0; + __int64 endTime64,freq64; + QueryPerformanceFrequency((LARGE_INTEGER *)&freq64); + QueryPerformanceCounter((LARGE_INTEGER *)&startTime64); +#endif +#endif + + m_maxZone = 1; // we start using zone 0 as a flag. + const Int maxZones=24000; + zoneStorageType zoneEquivalency[maxZones]; + Int i, j; + for (i=0; ibounds.hi.x || bounds.lo.y>bounds.hi.y) { + DEBUG_CRASH(("Incorrect bounds calculation. Logic error, fix me. jba.")); + continue; + } +#endif + m_zoneBlocks[xBlock][yBlock].setInteractsWithBridge(false); + for( j=bounds.lo.y; j<=bounds.hi.y; j++ ) { + for( i=bounds.lo.x; i<=bounds.hi.x; i++ ) { + PathfindCell *cell = &map[i][j]; + cell->setZone(0); + + if (i>bounds.lo.x) { + if (map[i][j].getType() == map[i-1][j].getType()) { + ZoneBlock::applyZone(map[i][j], map[i-1][j], zoneEquivalency, m_maxZone); + } + } + if (j>bounds.lo.y) { + if (map[i][j].getType() == map[i][j-1].getType()) { + ZoneBlock::applyZone(map[i][j], map[i][j - 1], zoneEquivalency, m_maxZone); + } + } + if (cell->getZone()==0) { + cell->setZone(m_maxZone); + m_maxZone++; +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + if (m_maxZone>= maxZones) { + DEBUG_CRASH(("Ran out of pathfind zones. SERIOUS ERROR! jba.")); + break; + } +#endif + } + if (cell->getConnectLayer() > LAYER_GROUND) { + m_zoneBlocks[xBlock][yBlock].setInteractsWithBridge(true); + } + + } + } + } + } + + Int totalZones = m_maxZone; + + // Collapse the zones into a 1,2,3... sequence, removing collapsed zones. + m_maxZone = 1; + Int collapsedZones[maxZones]; + collapsedZones[0] = 0; + for (i=1; ibounds.hi.x || bounds.lo.y>bounds.hi.y) { + DEBUG_CRASH(("Incorrect bounds calculation. Logic error, fix me. jba.")); + continue; + } +#endif + m_zoneBlocks[xBlock][yBlock].blockCalculateZones(map, layers, bounds); + } + } + + // Determine water/ground equivalent zones, and ground/cliff equivalent zones. + for (i=0; i LAYER_GROUND) && + (r_thisCell.getType() == PathfindCell::CELL_CLEAR) ) { + PathfindLayer *layer = layers + r_thisCell.getConnectLayer(); + ZoneBlock::resolveZones(r_thisCell.getZone(), layer->getZone(), m_hierarchicalZones, m_maxZone); + } + + if ( i > globalBounds.lo.x && r_thisCell.getZone() != map[i-1][j].getZone() ) { + const PathfindCell &r_leftCell = map[i-1][j]; + +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + if (r_thisCell.getType() == r_leftCell.getType()) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); + } + if (PathfindCell::waterGround(r_thisCell, r_leftCell)) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); + } + if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); + } + if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); + } + if (PathfindCell::terrain(r_thisCell, r_leftCell)) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); + } + if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); + } +#else + //if this is true, skip all the ones below + if (r_thisCell.getType() == r_leftCell.getType()) + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_hierarchicalZones, m_maxZone); + else { + Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below + + if (PathfindCell::terrain(r_thisCell, r_leftCell)) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_terrainZones, m_maxZone); + notTerrainOrCrusher = FALSE; + } + + if (PathfindCell::crusherGround(r_thisCell, r_leftCell)) { + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_crusherZones, m_maxZone); + notTerrainOrCrusher = FALSE; + } + + if ( notTerrainOrCrusher ) { + if (PathfindCell::waterGround(r_thisCell, r_leftCell)) + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundWaterZones, m_maxZone); + else if (PathfindCell::groundRubble(r_thisCell, r_leftCell)) + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundRubbleZones, m_maxZone); + else if (PathfindCell::groundCliff(r_thisCell, r_leftCell)) + ZoneBlock::applyZone(r_thisCell, r_leftCell, m_groundCliffZones, m_maxZone); + } + + } +#endif + + } + + if (j>globalBounds.lo.y && r_thisCell.getZone()!=map[i][j-1].getZone()) { + const PathfindCell &r_topCell = map[i][j-1]; + +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + if (r_thisCell.getType() == r_topCell.getType()) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); + } + if (PathfindCell::waterGround(r_thisCell, r_topCell)) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); + } + if (PathfindCell::groundRubble(r_thisCell, r_topCell)) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); + } + if (PathfindCell::groundCliff(r_thisCell, r_topCell)) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); + } + if (PathfindCell::terrain(r_thisCell, r_topCell)) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); + } + if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); + } +#else + //if this is true, skip all the ones below + if (r_thisCell.getType() == r_topCell.getType()) + ZoneBlock::applyZone(r_thisCell, r_topCell, m_hierarchicalZones, m_maxZone); + else { + Bool notTerrainOrCrusher = TRUE; // if this is false, skip the if-else-ladder below + + if (PathfindCell::terrain(r_thisCell, r_topCell)) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_terrainZones, m_maxZone); + notTerrainOrCrusher = FALSE; + } + + if (PathfindCell::crusherGround(r_thisCell, r_topCell)) { + ZoneBlock::applyZone(r_thisCell, r_topCell, m_crusherZones, m_maxZone); + notTerrainOrCrusher = FALSE; + } + + if (notTerrainOrCrusher) { + if (PathfindCell::waterGround(r_thisCell, r_topCell)) + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundWaterZones, m_maxZone); + else if (PathfindCell::groundRubble(r_thisCell, r_topCell)) + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundRubbleZones, m_maxZone); + else if (PathfindCell::groundCliff(r_thisCell, r_topCell)) + ZoneBlock::applyZone(r_thisCell, r_topCell, m_groundCliffZones, m_maxZone); + } + + } +#endif + + } + + } + } + + //FLATTEN HIERARCHICAL ZONES + for (i=1; im_debugAI == AI_DEBUG_ZONES) + { + extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); + RGBColor color; + memset(&color, 0, sizeof(Color)); + addIcon(nullptr, 0, 0, color); + for( j=0; jgetLayerHeight( pos.x, pos.y, map[i][j].getLayer() ) + 0.5f; + addIcon(&pos, PATHFIND_CELL_SIZE_F*0.8f, 500, color); + } + } + } +#endif + m_nextFrameToCalculateZones = 0xffffffff; +} + +/** + * Update zones where a structure has been added or removed. + * This can be done by just updating the equivalency arrays, without rezoning the map.. + */ +void PathfindZoneManager::updateZonesForModify(PathfindCell **map, PathfindLayer layers[], const IRegion2D &structureBounds, const IRegion2D &globalBounds ) +{ + +#ifdef DEBUG_QPF +#if defined(DEBUG_LOGGING) + __int64 startTime64; + double timeToUpdate=0.0f; + __int64 endTime64,freq64; + QueryPerformanceFrequency((LARGE_INTEGER *)&freq64); + QueryPerformanceCounter((LARGE_INTEGER *)&startTime64); +#endif +#endif + IRegion2D bounds = structureBounds; + bounds.hi.x++; + bounds.hi.y++; + bounds.hi.updateMin(globalBounds.hi); + + Int xBlock, yBlock; + for (xBlock = 0; xBlockblockBounds.hi.x || blockBounds.lo.y>blockBounds.hi.y) { + continue; + } + m_zoneBlocks[xBlock][yBlock].setInteractsWithBridge(false); + Int i, j; + for( j=blockBounds.lo.y; j<=blockBounds.hi.y; j++ ) { + for( i=blockBounds.lo.x; i<=blockBounds.hi.x; i++ ) { + PathfindCell *cell = &map[i][j]; + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + + if (i>blockBounds.lo.x) { + if (map[i][j].getType() == map[i-1][j].getType()) { + cell->setZone(map[i-1][j].getZone()); + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + } + } + if (j>blockBounds.lo.y) { + if (cell->getType() == map[i][j-1].getType()) { + cell->setZone(map[i][j-1].getZone()); + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + } + if (isetZone(map[i+1][j-1].getZone()); + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + } + } + } + } + } + for( j=blockBounds.hi.y; j>=blockBounds.lo.y; j-- ) { + for( i=blockBounds.hi.x; i>=blockBounds.lo.x; i-- ) { + PathfindCell *cell = &map[i][j]; + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + if (isetZone(map[i+1][j].getZone()); + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + } + } + if (jgetType() == map[i][j+1].getType()) { + cell->setZone(map[i][j+1].getZone()); + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + } + if (isetZone(map[i+1][j+1].getZone()); + if (cell->getZone()!=UNINITIALIZED_ZONE) continue; + } + } + } + } + } + } + } +#ifdef DEBUG_QPF +#if defined(DEBUG_LOGGING) + QueryPerformanceCounter((LARGE_INTEGER *)&endTime64); + timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64)); +#endif +#endif +#if defined(RTS_DEBUG) + if (TheGlobalData->m_debugAI==AI_DEBUG_ZONES) + { + extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); + RGBColor color; + memset(&color, 0, sizeof(Color)); + addIcon(nullptr, 0, 0, color); + Int i, j; + for( j=0; jgetLayerHeight( pos.x, pos.y, map[i][j].getLayer() ) + 0.5f; + addIcon(&pos, PATHFIND_CELL_SIZE_F*0.8f, 200, color); + } + } + } +#endif + +} + +// +// Clear the passable flags. +// +void PathfindZoneManager::clearPassableFlags() +{ Int blockX; + Int blockY; + for (blockX = 0; blockX=m_zoneBlockExtent.x) { + DEBUG_CRASH(("Invalid block.")); + return; + } + if (blockY<0 || blockY>=m_zoneBlockExtent.y) { + DEBUG_CRASH(("Invalid block.")); + return; + } + m_zoneBlocks[blockX][blockY].setPassable(passable); +} + +// +// Get the passable flag for the block at this location. +// +Bool PathfindZoneManager::isPassable(Int cellX, Int cellY) const +{ + Int blockX = cellX/ZONE_BLOCK_SIZE; + Int blockY = cellY/ZONE_BLOCK_SIZE; + + if (blockX<0 || blockX>=m_zoneBlockExtent.x) { + DEBUG_CRASH(("Invalid block.")); + return false; + } + if (blockY<0 || blockY>=m_zoneBlockExtent.y) { + DEBUG_CRASH(("Invalid block.")); + return false; + } + return m_zoneBlocks[blockX][blockY].isPassable(); +} + +// +// Get the passable flag for the block at this location. +// +Bool PathfindZoneManager::clipIsPassable(Int cellX, Int cellY) const +{ + Int blockX = cellX/ZONE_BLOCK_SIZE; + Int blockY = cellY/ZONE_BLOCK_SIZE; + + if (blockX<0 || blockX>=m_zoneBlockExtent.x) { + return false; + } + if (blockY<0 || blockY>=m_zoneBlockExtent.y) { + return false; + } + return m_zoneBlocks[blockX][blockY].isPassable(); +} + +// +// Set the bridge flag for the block at this location. +// +void PathfindZoneManager::setBridge(Int cellX, Int cellY, Bool bridge) +{ + Int blockX = cellX/ZONE_BLOCK_SIZE; + Int blockY = cellY/ZONE_BLOCK_SIZE; + + if (blockX<0 || blockX>=m_zoneBlockExtent.x) { + // DEBUG_CRASH(("Invalid block.")); Bridges can be off the playable grid, so don't crash. jba. + return; + } + if (blockY<0 || blockY>=m_zoneBlockExtent.y) { + // DEBUG_CRASH(("Invalid block.")); Bridges can be off the playable grid, so don't crash. jba. + return; + } + m_zoneBlocks[blockX][blockY].setInteractsWithBridge(bridge); +} + + +// +// Set the bridge flag for the block at this location. +// +Bool PathfindZoneManager::interactsWithBridge(Int cellX, Int cellY) const +{ + Int blockX = cellX/ZONE_BLOCK_SIZE; + Int blockY = cellY/ZONE_BLOCK_SIZE; + + if (blockX<0 || blockX>=m_zoneBlockExtent.x) { + DEBUG_CRASH(("Invalid block.")); + return false; + } + if (blockY<0 || blockY>=m_zoneBlockExtent.y) { + DEBUG_CRASH(("Invalid block.")); + return false; + } + return m_zoneBlocks[blockX][blockY].getInteractsWithBridge(); +} + + +// +// Return the zone at this location. +// +zoneStorageType PathfindZoneManager::getBlockZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher,Int cellX, Int cellY, PathfindCell **map) const +{ + PathfindCell *cell = &(map[cellX][cellY]); + Int blockX = cellX/ZONE_BLOCK_SIZE; + Int blockY = cellY/ZONE_BLOCK_SIZE; + + if (blockX<0 || blockX>=m_zoneBlockExtent.x) { + DEBUG_CRASH(("Invalid block.")); + return 0; + } + if (blockY<0 || blockY>=m_zoneBlockExtent.y) { + DEBUG_CRASH(("Invalid block.")); + return 0; + } + zoneStorageType zone = m_zoneBlocks[blockX][blockY].getEffectiveZone(acceptableSurfaces, crusher, cell->getZone()); +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + if (zone > m_maxZone) { +#else + if (zone >= m_maxZone) { +#endif + DEBUG_CRASH(("Invalid zone.")); + return UNINITIALIZED_ZONE; + } + return zone; +} + +// +// Return the zone at this location. +// +zoneStorageType PathfindZoneManager::getEffectiveTerrainZone(zoneStorageType zone) const +{ + return m_hierarchicalZones[m_terrainZones[zone]]; +} + +// +// Return the zone at this location. +// +zoneStorageType PathfindZoneManager::getEffectiveZone( LocomotorSurfaceTypeMask acceptableSurfaces, + Bool crusher, zoneStorageType zone) const +{ + //DEBUG_ASSERTCRASH(zone, ("Zone not set")); + if (zone>m_maxZone) { + DEBUG_CRASH(("Invalid zone")); + return (0); + } + if (zone>m_maxZone) { + DEBUG_CRASH(("Invalid zone")); + return (0); + } + if (acceptableSurfaces&LOCOMOTORSURFACE_AIR) return 1; // air is all zone 1. + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_WATER) && + (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { + // Locomotors can go on ground, water & cliff, so all is zone 1. + return 1; + } + + if (crusher) { + zone = m_crusherZones[zone]; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF)) { + // Locomotors can go on ground & cliff, so use the ground cliff combiner. + zone = m_groundCliffZones[zone]; + return zone; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { + // Locomotors can go on ground & water, so use the ground water combiner. + zone = m_groundWaterZones[zone]; + return zone; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_GROUND) && + (acceptableSurfaces&LOCOMOTORSURFACE_RUBBLE)) { + // Locomotors can go on ground & rubble, so use the ground rubble combiner. + zone = m_groundRubbleZones[zone]; + return zone; + } + + if ( (acceptableSurfaces&LOCOMOTORSURFACE_CLIFF) && + (acceptableSurfaces&LOCOMOTORSURFACE_WATER)) { + // Locomotors can go on ground & cliff, so use the ground cliff combiner. + DEBUG_CRASH(("Cliff water only locomotor sets not supported yet.")); + } + zone = m_hierarchicalZones[zone]; + + return zone; +} From 2c35e3456c29b6e237f2c042c981ba44e285827a Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:00:07 +0200 Subject: [PATCH 10/10] refactor(pathfinder): Move PathfindLayer to its own file (#3429) --- Core/GameEngine/CMakeLists.txt | 1 + .../Source/GameLogic/AI/AIPathfind.cpp | 651 ----------------- .../GameLogic/AI/Pathfinder/PathfindLayer.cpp | 676 ++++++++++++++++++ 3 files changed, 677 insertions(+), 651 deletions(-) create mode 100644 Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindLayer.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 4e4dd4fbdce..c5ee90c5cb8 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -868,6 +868,7 @@ set(GAMEENGINE_SRC Source/GameLogic/AI/Pathfinder/PathfindCellInfo.cpp Source/GameLogic/AI/Pathfinder/PathfindCellList.cpp Source/GameLogic/AI/Pathfinder/PathfindConstants.cpp + Source/GameLogic/AI/Pathfinder/PathfindLayer.cpp Source/GameLogic/AI/Pathfinder/PathfindZoneManager.cpp Source/GameLogic/AI/Pathfinder/PathNode.cpp Source/GameLogic/AI/Pathfinder/ZoneBlock.cpp diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 92b2721d11f..3434b3ced42 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -147,657 +147,6 @@ void Pathfinder::forceCleanCells() } #endif -//-------------------- PathfindLayer ---------------------------------------- -PathfindLayer::PathfindLayer() : m_blockOfMapCells(nullptr), m_layerCells(nullptr), m_bridge(nullptr), -m_destroyed(FALSE), -m_height(0), -m_width(0), -m_xOrigin(0), -m_yOrigin(0), -m_zone(0) -{ - m_startCell.x = -1; - m_startCell.y = -1; - m_endCell.x = -1; - m_endCell.y = -1; -} - -PathfindLayer::~PathfindLayer() -{ - reset(); -} - -/** - * Returns true if the layer is available for use. - */ -void PathfindLayer::reset() -{ - m_bridge = nullptr; - if (m_layerCells) { - Int i, j; - for (i=0; ireset(); - } - } - delete [] m_layerCells; - m_layerCells = nullptr; - } - - delete [] m_blockOfMapCells; - m_blockOfMapCells = nullptr; - - m_width = 0; - m_height = 0; - m_xOrigin = 0; - m_yOrigin = 0; - m_startCell.x = -1; - m_startCell.y = -1; - m_endCell.x = -1; - m_endCell.y = -1; - m_layer = LAYER_GROUND; -} - -/** - * Returns true if the layer is available for use. - */ -Bool PathfindLayer::isUnused() -{ - // Special case - wall layer is built from not a bridge. jba. - if (m_layer == LAYER_WALL && m_width>0) return false; - - if (m_bridge==nullptr) return true; - return false; -} - - - -/** - * Draws debug cell info. - */ -#if defined(RTS_DEBUG) -void PathfindLayer::doDebugIcons() { - if (isUnused()) return; - extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); - // render AI debug information - { - Coord3D topLeftCorner; - RGBColor color; - color.red = color.green = color.blue = 0; - Coord3D center; - center.x = (m_xOrigin+m_width/2)*PATHFIND_CELL_SIZE_F; - center.y = (m_yOrigin+m_height/2)*PATHFIND_CELL_SIZE_F; - center.z = 0; - Real bridgeHeight = TheTerrainLogic->getLayerHeight(center.x , center.y, m_layer); - if (m_layer == LAYER_WALL) { - bridgeHeight = TheAI->pathfinder()->getWallHeight(); - } - static Int flash = 0; - flash--; - if (flash<1) flash = 20; - if (flash < 10) return; - Bool showCells = TheGlobalData->m_debugAI==AI_DEBUG_CELLS; - // show the pathfind grid - for( int j=0; jgetConnectLayer()==LAYER_GROUND) { - color.green = 1; - color.blue = 1; - empty = false; - } else if (cell->getType() == PathfindCell::CELL_IMPASSABLE) { - color.red = color.green = color.blue = 1; - size = 0.2f; - empty = false; - } else if (cell->getType() == PathfindCell::CELL_BRIDGE_IMPASSABLE) { - color.blue = color.red = 1; - empty = false; - } else if (cell->getType() == PathfindCell::CELL_CLIFF) { - color.red = 1; - empty = false; - } else { - size = 0.2f; - } - } - if (showCells) { - empty = true; - color.red = color.green = color.blue = 0; - if (empty && cell) { - if (cell->getFlags()!=PathfindCell::NO_UNITS) { - empty = false; - if (cell->getFlags() == PathfindCell::UNIT_GOAL) { - color.red = 1; - } else if (cell->getFlags() == PathfindCell::UNIT_PRESENT_FIXED) { - color.green = color.blue = color.red = 1; - } else if (cell->getFlags() == PathfindCell::UNIT_PRESENT_MOVING) { - color.green = 1; - } else { - color.green = color.red = 1; - } - } - } - } - if (!empty) { - Coord3D loc; - loc.x = topLeftCorner.x + PATHFIND_CELL_SIZE_F/2.0f; - loc.y = topLeftCorner.y + PATHFIND_CELL_SIZE_F/2.0f; - loc.z = bridgeHeight; - addIcon(&loc, PATHFIND_CELL_SIZE_F*size, 99, color); - } - } - } - - } -} -#endif - -/** - * Sets the bridge & layer number for a layer. - */ -Bool PathfindLayer::init(Bridge *theBridge, PathfindLayerEnum layer) -{ - if (m_bridge!=nullptr) return false; - m_bridge = theBridge; - m_layer = layer; - m_destroyed = false; - return true; -} - -/** - * Allocates the pathfind cells for the bridge layer. - */ -void PathfindLayer::allocateCells(const IRegion2D *extent) -{ - if (m_bridge == nullptr) return; - Region2D bridgeBounds = *m_bridge->getBounds(); - Int maxX, maxY; - m_xOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.x-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - m_yOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.y-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - m_width = 0; - m_height = 0; - maxX = REAL_TO_INT_CEIL((bridgeBounds.hi.x+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - maxY = REAL_TO_INT_CEIL((bridgeBounds.hi.y+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - // Pad with 1 extra; - m_xOrigin--; - m_yOrigin--; - maxX++; - maxY++; - - if (m_xOrigin < extent->lo.x) m_xOrigin = extent->lo.x; - if (m_yOrigin < extent->lo.y) m_yOrigin = extent->lo.y; - if (maxX > extent->hi.x) maxX = extent->hi.x; - if (maxY > extent->hi.y) maxY = extent->hi.y; - if (maxX <= m_xOrigin) return; - if (maxY <= m_yOrigin) return; - m_width = maxX - m_xOrigin; - m_height = maxY - m_yOrigin; - - // Allocate cells. - // pool[]ify - m_blockOfMapCells = MSGNEW("PathfindMapCells") PathfindCell[m_width*m_height]; - m_layerCells = MSGNEW("PathfindMapCells") PathfindCellP[m_width]; - Int i; - for (i=0; ifindObjectByID(wallPieces[i]); - Region2D objBounds; - if (obj==nullptr) continue; - obj->getGeometryInfo().get2DBounds(*obj->getPosition(), obj->getOrientation(), objBounds); - if (first) { - bridgeBounds = objBounds; - first = false; - } else { - bridgeBounds.uniteWith(objBounds); - } - } - - Int maxX, maxY; - m_xOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.x-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - m_yOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.y-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - m_width = 0; - m_height = 0; - maxX = REAL_TO_INT_CEIL((bridgeBounds.hi.x+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - maxY = REAL_TO_INT_CEIL((bridgeBounds.hi.y+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); - // Pad with 1 extra; - m_xOrigin--; - m_yOrigin--; - maxX++; - maxY++; - - if (m_xOrigin < extent->lo.x) m_xOrigin = extent->lo.x; - if (m_yOrigin < extent->lo.y) m_yOrigin = extent->lo.y; - if (maxX > extent->hi.x) maxX = extent->hi.x; - if (maxY > extent->hi.y) maxY = extent->hi.y; - if (maxX <= m_xOrigin) return; - if (maxY <= m_yOrigin) return; - m_width = maxX - m_xOrigin; - m_height = maxY - m_yOrigin; - - // Allocate cells. - m_blockOfMapCells = MSGNEW("PathfindMapCells") PathfindCell[m_width*m_height]; - m_layerCells = MSGNEW("PathfindMapCells") PathfindCellP[m_width]; - - for (i=0; igetConnectLayer()==LAYER_GROUND) { - PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i+m_xOrigin, j+m_yOrigin); - DEBUG_ASSERTCRASH(groundCell, ("Should have cell.")); - if (groundCell) { - zoneStorageType zone = zm->getEffectiveZone(locoSet.getValidSurfaces(), - true, groundCell->getZone()); - zone = zm->getEffectiveTerrainZone(zone); - if (zone == zone1) found1 = true; - if (zone == zone2) found2 = true; - } - } - } - } - return found1 && found2; -} - -/** - * Classifies the pathfind cells for the bridge layer. - */ -void PathfindLayer::classifyCells() -{ - m_startCell.x = -1; - m_startCell.y = -1; - m_endCell.x = -1; - m_endCell.y = -1; - Int i, j; - for (i=0; isetConnectLayer(LAYER_INVALID); - cell->setLayer(m_layer); - classifyLayerMapCell(i+m_xOrigin, j+m_yOrigin, cell, m_bridge); - } - BridgeInfo info; - m_bridge->getBridgeInfo(&info); - Coord3D bridgeDir = info.to; - bridgeDir.x -= info.from.x; - bridgeDir.y -= info.from.y; - bridgeDir.z -= info.from.z; - bridgeDir.normalize(); - bridgeDir.x *= PATHFIND_CELL_SIZE_F*0.7f; - bridgeDir.y *= PATHFIND_CELL_SIZE_F*0.7f; - - m_startCell.x = REAL_TO_INT_FLOOR((info.from.x-bridgeDir.x) / PATHFIND_CELL_SIZE_F); - m_startCell.y = REAL_TO_INT_FLOOR((info.from.y-bridgeDir.y) / PATHFIND_CELL_SIZE_F); - m_endCell.x = REAL_TO_INT_FLOOR((info.to.x+bridgeDir.x) / PATHFIND_CELL_SIZE_F); - m_endCell.y = REAL_TO_INT_FLOOR((info.to.y+bridgeDir.y) / PATHFIND_CELL_SIZE_F); - } - if (m_destroyed) { - Int i, j; - for (i=0; igetConnectLayer() == LAYER_GROUND) { - PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i+m_xOrigin, j+m_yOrigin); - DEBUG_ASSERTCRASH(groundCell, ("Should have cell.")); - if (groundCell) { - DEBUG_ASSERTCRASH(groundCell->getConnectLayer()==m_layer, ("Should connect to this layer.jba.")); - groundCell->setConnectLayer(LAYER_INVALID); // disconnect it. - } - } -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - cell->setType(PathfindCell::CELL_IMPASSABLE); -#else - cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); -#endif - } - } - } -} - -/** - * Classifies the pathfind cells for the wall bridge layer. - */ -void PathfindLayer::classifyWallCells(ObjectID *wallPieces, Int numPieces) -{ - DEBUG_ASSERTCRASH(m_layer==LAYER_WALL, ("Wrong layer for wall.")); - if (m_layer != LAYER_WALL) return; - if (m_layerCells == nullptr) return; - - Int i, j; - for (i=0; isetConnectLayer(LAYER_INVALID); - cell->setLayer(m_layer); - classifyWallMapCell(i+m_xOrigin, j+m_yOrigin, cell, wallPieces, numPieces); - cell->setPinched(false); - } - } - if (m_destroyed) { - Int i, j; - for (i=0; igetConnectLayer() == LAYER_GROUND) { - PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i+m_xOrigin, j+m_yOrigin); - DEBUG_ASSERTCRASH(groundCell, ("Should have cell.")); - if (groundCell) { - DEBUG_ASSERTCRASH(groundCell->getConnectLayer()==m_layer, ("Should connect to this layer.jba.")); - groundCell->setConnectLayer(LAYER_INVALID); // disconnect it. - } - } - cell->setType(PathfindCell::CELL_IMPASSABLE); - } - } - } - - // Tighten up 1 cell. - for (i=1; igetType() != PathfindCell::CELL_CLEAR) { - cell->setPinched(true); - } - } - } - } - } - for (i=0; igetPinched() && cell->getType() == PathfindCell::CELL_CLEAR) { - cell->setType(PathfindCell::CELL_CLIFF); - } - cell->setPinched(false); - } - } -} - -/** - * Relassifies the pathfind cells for the destroyed bridge layer. - */ -Bool PathfindLayer::setDestroyed(Bool destroyed) -{ - if (destroyed == m_destroyed) return false; - - m_destroyed = destroyed; - classifyCells(); - - return true; -} - -/** - * Copies m_zone into the zone for all the member cells. - */ -void PathfindLayer::applyZone() -{ - Int i, j; - for (i=0; isetZone(m_zone); - } - } -} - - -/** - * Return the bridge's object id. - */ -ObjectID PathfindLayer::getBridgeID() -{ - return m_bridge->peekBridgeInfo()->bridgeObjectID; -} - -/** - * Return the cell at the index location. - */ -PathfindCell *PathfindLayer::getCell(Int x, Int y) -{ - DEBUG_ASSERTCRASH(m_layerCells, ("no data in layer, why get cells?")); - if (m_layerCells==nullptr) { - return nullptr; - } - x -= m_xOrigin; - y -= m_yOrigin; - if (x<0 || x>=m_width) return nullptr; - if (y<0 || y>=m_height) return nullptr; - PathfindCell *cell = &m_layerCells[x][y]; - if (cell->getType() == PathfindCell::CELL_IMPASSABLE) { - return nullptr; // Impassable cells are ignored. - } - return cell; -} - - -/** - * Classify the given map cell as clear, or not, etc. - */ -void PathfindLayer::classifyLayerMapCell( Int i, Int j , PathfindCell *cell, Bridge *theBridge) -{ - Coord3D topLeftCorner, bottomRightCorner; - - topLeftCorner.y = (Real)j * PATHFIND_CELL_SIZE_F; - bottomRightCorner.y = topLeftCorner.y + PATHFIND_CELL_SIZE_F; - - topLeftCorner.x = (Real)i * PATHFIND_CELL_SIZE_F; - bottomRightCorner.x = topLeftCorner.x + PATHFIND_CELL_SIZE_F; - - - Int bridgeCount = 0; - Coord3D pt; - if (theBridge->isPointOnBridge(&topLeftCorner) ) { - bridgeCount++; - } - pt = topLeftCorner; - pt.y = bottomRightCorner.y; - if (theBridge->isPointOnBridge(&pt) ) { - bridgeCount++; - } - if (theBridge->isPointOnBridge(&bottomRightCorner) ) { - bridgeCount++; - } - pt = topLeftCorner; - pt.x = bottomRightCorner.x; - if (theBridge->isPointOnBridge(&pt) ) { - bridgeCount++; - } - cell->reset(); - cell->setLayer(m_layer); - cell->setType(PathfindCell::CELL_IMPASSABLE); - if (bridgeCount == 4) { - cell->setType(PathfindCell::CELL_CLEAR); - } else { - if (bridgeCount!=0) { -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - cell->setType(PathfindCell::CELL_CLIFF); // it's off the bridge. -#else - cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); // it's off the bridge. -#endif - } - - // check against the end lines. - - Region2D cellBounds; - cellBounds.lo = topLeftCorner.asCoord2D(); - cellBounds.hi = bottomRightCorner.asCoord2D(); - -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - if (m_bridge->isCellOnEnd(&cellBounds)) { - cell->setType(PathfindCell::CELL_CLEAR); - } - if (m_bridge->isCellOnSide(&cellBounds)) { - cell->setType(PathfindCell::CELL_CLIFF); - } else { - if (m_bridge->isCellEntryPoint(&cellBounds)) { - cell->setType(PathfindCell::CELL_CLEAR); - cell->setConnectLayer(LAYER_GROUND); - PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i, j ); - groundCell->setConnectLayer(cell->getLayer()); - } - } -#else - if (m_bridge->isCellOnSide(&cellBounds)) { - cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); - } else { - if (m_bridge->isCellOnEnd(&cellBounds)) { - cell->setType(PathfindCell::CELL_CLEAR); - } - if (m_bridge->isCellEntryPoint(&cellBounds)) { - cell->setType(PathfindCell::CELL_CLEAR); - cell->setConnectLayer(LAYER_GROUND); - PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i, j ); - groundCell->setConnectLayer(cell->getLayer()); - } - } -#endif - } - Coord3D center = topLeftCorner; - center.x += PATHFIND_CELL_SIZE/2; - center.y += PATHFIND_CELL_SIZE/2; - if (cell->getType()!=PathfindCell::CELL_IMPASSABLE) { - if (!(cell->getConnectLayer()==LAYER_GROUND) ) { - // Check for bridge clearance. If the ground isn't 1 pathfind cells below, mark impassable. - Real groundHeight = TheTerrainLogic->getLayerHeight( center.x, center.y, LAYER_GROUND ); - Real bridgeHeight = theBridge->getBridgeHeight( ¢er, nullptr ); - if (groundHeight+LAYER_Z_CLOSE_ENOUGH_F > bridgeHeight) { - PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND,i, j); - if (!(groundCell->getType()==PathfindCell::CELL_OBSTACLE)) { -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - groundCell->setType(PathfindCell::CELL_IMPASSABLE); -#else - groundCell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); -#endif - } - } - } - } -} - - -Bool PathfindLayer::isPointOnWall(ObjectID *wallPieces, Int numPieces, const Coord3D *pt) -{ - Int i; - for (i=0; ifindObjectByID(wallPieces[i]); - if (obj==nullptr) continue; - Real major = obj->getGeometryInfo().getMajorRadius(); - Real minor = (obj->getGeometryInfo().getGeomType() == GEOMETRY_SPHERE) ? obj->getGeometryInfo().getMajorRadius() : obj->getGeometryInfo().getMinorRadius(); - - Real c = (Real)Cos(-obj->getOrientation()); - Real s = (Real)Sin(-obj->getOrientation()); - - // convert to a delta relative to rect ctr - Real ptx = pt->x - obj->getPosition()->x; - Real pty = pt->y - obj->getPosition()->y; - - // inverse-rotate it to the right coord system - Real ptx_new = (Real)fabs(ptx*c - pty*s); - Real pty_new = (Real)fabs(ptx*s + pty*c); - - if (ptx_new <= major && pty_new <= minor) - { - return true; - } - } - return false; -} - - -/** - * Classify the given map cell as clear, or not, etc. - */ -void PathfindLayer::classifyWallMapCell( Int i, Int j , PathfindCell *cell, ObjectID *wallPieces, Int numPieces) -{ - Coord3D topLeftCorner, bottomRightCorner; - - topLeftCorner.y = (Real)j * PATHFIND_CELL_SIZE_F; - bottomRightCorner.y = topLeftCorner.y + PATHFIND_CELL_SIZE_F; - - topLeftCorner.x = (Real)i * PATHFIND_CELL_SIZE_F; - bottomRightCorner.x = topLeftCorner.x + PATHFIND_CELL_SIZE_F; - - - Int bridgeCount = 0; - Coord3D pt; - if (isPointOnWall(wallPieces, numPieces, &topLeftCorner) ) { - bridgeCount++; - } - pt = topLeftCorner; - pt.y = bottomRightCorner.y; - if (isPointOnWall(wallPieces, numPieces, &pt) ) { - bridgeCount++; - } - if (isPointOnWall(wallPieces, numPieces, &bottomRightCorner) ) { - bridgeCount++; - } - pt = topLeftCorner; - pt.x = bottomRightCorner.x; - if (isPointOnWall(wallPieces, numPieces, &pt) ) { - bridgeCount++; - } - cell->reset(); - cell->setLayer(m_layer); - cell->setType(PathfindCell::CELL_IMPASSABLE); - if (bridgeCount == 4) { - cell->setType(PathfindCell::CELL_CLEAR); - } else { - if (bridgeCount!=0) { -#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING - cell->setType(PathfindCell::CELL_CLIFF); // it's off the bridge. -#else - cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); // it's off the bridge. -#endif - } - - } -} - //----------------------- Pathfinder --------------------------------------- Pathfinder::Pathfinder() :m_map(nullptr) diff --git a/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindLayer.cpp b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindLayer.cpp new file mode 100644 index 00000000000..7a1901172e0 --- /dev/null +++ b/Core/GameEngine/Source/GameLogic/AI/Pathfinder/PathfindLayer.cpp @@ -0,0 +1,676 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "GameLogic/AIPathfind.h" +#include "GameLogic/GameLogic.h" +#include "GameLogic/Object.h" +#include "GameLogic/Pathfinder/PathfindCell.h" +#include "GameLogic/Pathfinder/PathfindConstants.h" +#include "GameLogic/Pathfinder/PathfindLayer.h" +#include "GameLogic/Pathfinder/PathfindZoneManager.h" +#include "GameLogic/TerrainLogic.h" + +PathfindLayer::PathfindLayer() : m_blockOfMapCells(nullptr), m_layerCells(nullptr), m_bridge(nullptr), +m_destroyed(FALSE), +m_height(0), +m_width(0), +m_xOrigin(0), +m_yOrigin(0), +m_zone(0) +{ + m_startCell.x = -1; + m_startCell.y = -1; + m_endCell.x = -1; + m_endCell.y = -1; +} + +PathfindLayer::~PathfindLayer() +{ + reset(); +} + +/** + * Returns true if the layer is available for use. + */ +void PathfindLayer::reset() +{ + m_bridge = nullptr; + if (m_layerCells) { + Int i, j; + for (i=0; ireset(); + } + } + delete [] m_layerCells; + m_layerCells = nullptr; + } + + delete [] m_blockOfMapCells; + m_blockOfMapCells = nullptr; + + m_width = 0; + m_height = 0; + m_xOrigin = 0; + m_yOrigin = 0; + m_startCell.x = -1; + m_startCell.y = -1; + m_endCell.x = -1; + m_endCell.y = -1; + m_layer = LAYER_GROUND; +} + +/** + * Returns true if the layer is available for use. + */ +Bool PathfindLayer::isUnused() +{ + // Special case - wall layer is built from not a bridge. jba. + if (m_layer == LAYER_WALL && m_width>0) return false; + + if (m_bridge==nullptr) return true; + return false; +} + + + +/** + * Draws debug cell info. + */ +#if defined(RTS_DEBUG) +void PathfindLayer::doDebugIcons() { + if (isUnused()) return; + extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); + // render AI debug information + { + Coord3D topLeftCorner; + RGBColor color; + color.red = color.green = color.blue = 0; + Coord3D center; + center.x = (m_xOrigin+m_width/2)*PATHFIND_CELL_SIZE_F; + center.y = (m_yOrigin+m_height/2)*PATHFIND_CELL_SIZE_F; + center.z = 0; + Real bridgeHeight = TheTerrainLogic->getLayerHeight(center.x , center.y, m_layer); + if (m_layer == LAYER_WALL) { + bridgeHeight = TheAI->pathfinder()->getWallHeight(); + } + static Int flash = 0; + flash--; + if (flash<1) flash = 20; + if (flash < 10) return; + Bool showCells = TheGlobalData->m_debugAI==AI_DEBUG_CELLS; + // show the pathfind grid + for( int j=0; jgetConnectLayer()==LAYER_GROUND) { + color.green = 1; + color.blue = 1; + empty = false; + } else if (cell->getType() == PathfindCell::CELL_IMPASSABLE) { + color.red = color.green = color.blue = 1; + size = 0.2f; + empty = false; + } else if (cell->getType() == PathfindCell::CELL_BRIDGE_IMPASSABLE) { + color.blue = color.red = 1; + empty = false; + } else if (cell->getType() == PathfindCell::CELL_CLIFF) { + color.red = 1; + empty = false; + } else { + size = 0.2f; + } + } + if (showCells) { + empty = true; + color.red = color.green = color.blue = 0; + if (empty && cell) { + if (cell->getFlags()!=PathfindCell::NO_UNITS) { + empty = false; + if (cell->getFlags() == PathfindCell::UNIT_GOAL) { + color.red = 1; + } else if (cell->getFlags() == PathfindCell::UNIT_PRESENT_FIXED) { + color.green = color.blue = color.red = 1; + } else if (cell->getFlags() == PathfindCell::UNIT_PRESENT_MOVING) { + color.green = 1; + } else { + color.green = color.red = 1; + } + } + } + } + if (!empty) { + Coord3D loc; + loc.x = topLeftCorner.x + PATHFIND_CELL_SIZE_F/2.0f; + loc.y = topLeftCorner.y + PATHFIND_CELL_SIZE_F/2.0f; + loc.z = bridgeHeight; + addIcon(&loc, PATHFIND_CELL_SIZE_F*size, 99, color); + } + } + } + + } +} +#endif + +/** + * Sets the bridge & layer number for a layer. + */ +Bool PathfindLayer::init(Bridge *theBridge, PathfindLayerEnum layer) +{ + if (m_bridge!=nullptr) return false; + m_bridge = theBridge; + m_layer = layer; + m_destroyed = false; + return true; +} + +/** + * Allocates the pathfind cells for the bridge layer. + */ +void PathfindLayer::allocateCells(const IRegion2D *extent) +{ + if (m_bridge == nullptr) return; + Region2D bridgeBounds = *m_bridge->getBounds(); + Int maxX, maxY; + m_xOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.x-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + m_yOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.y-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + m_width = 0; + m_height = 0; + maxX = REAL_TO_INT_CEIL((bridgeBounds.hi.x+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + maxY = REAL_TO_INT_CEIL((bridgeBounds.hi.y+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + // Pad with 1 extra; + m_xOrigin--; + m_yOrigin--; + maxX++; + maxY++; + + if (m_xOrigin < extent->lo.x) m_xOrigin = extent->lo.x; + if (m_yOrigin < extent->lo.y) m_yOrigin = extent->lo.y; + if (maxX > extent->hi.x) maxX = extent->hi.x; + if (maxY > extent->hi.y) maxY = extent->hi.y; + if (maxX <= m_xOrigin) return; + if (maxY <= m_yOrigin) return; + m_width = maxX - m_xOrigin; + m_height = maxY - m_yOrigin; + + // Allocate cells. + // pool[]ify + m_blockOfMapCells = MSGNEW("PathfindMapCells") PathfindCell[m_width*m_height]; + m_layerCells = MSGNEW("PathfindMapCells") PathfindCellP[m_width]; + Int i; + for (i=0; ifindObjectByID(wallPieces[i]); + Region2D objBounds; + if (obj==nullptr) continue; + obj->getGeometryInfo().get2DBounds(*obj->getPosition(), obj->getOrientation(), objBounds); + if (first) { + bridgeBounds = objBounds; + first = false; + } else { + bridgeBounds.uniteWith(objBounds); + } + } + + Int maxX, maxY; + m_xOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.x-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + m_yOrigin = REAL_TO_INT_FLOOR((bridgeBounds.lo.y-PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + m_width = 0; + m_height = 0; + maxX = REAL_TO_INT_CEIL((bridgeBounds.hi.x+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + maxY = REAL_TO_INT_CEIL((bridgeBounds.hi.y+PATHFIND_CELL_SIZE/100)/PATHFIND_CELL_SIZE); + // Pad with 1 extra; + m_xOrigin--; + m_yOrigin--; + maxX++; + maxY++; + + if (m_xOrigin < extent->lo.x) m_xOrigin = extent->lo.x; + if (m_yOrigin < extent->lo.y) m_yOrigin = extent->lo.y; + if (maxX > extent->hi.x) maxX = extent->hi.x; + if (maxY > extent->hi.y) maxY = extent->hi.y; + if (maxX <= m_xOrigin) return; + if (maxY <= m_yOrigin) return; + m_width = maxX - m_xOrigin; + m_height = maxY - m_yOrigin; + + // Allocate cells. + m_blockOfMapCells = MSGNEW("PathfindMapCells") PathfindCell[m_width*m_height]; + m_layerCells = MSGNEW("PathfindMapCells") PathfindCellP[m_width]; + + for (i=0; igetConnectLayer()==LAYER_GROUND) { + PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i+m_xOrigin, j+m_yOrigin); + DEBUG_ASSERTCRASH(groundCell, ("Should have cell.")); + if (groundCell) { + zoneStorageType zone = zm->getEffectiveZone(locoSet.getValidSurfaces(), + true, groundCell->getZone()); + zone = zm->getEffectiveTerrainZone(zone); + if (zone == zone1) found1 = true; + if (zone == zone2) found2 = true; + } + } + } + } + return found1 && found2; +} + +/** + * Classifies the pathfind cells for the bridge layer. + */ +void PathfindLayer::classifyCells() +{ + m_startCell.x = -1; + m_startCell.y = -1; + m_endCell.x = -1; + m_endCell.y = -1; + Int i, j; + for (i=0; isetConnectLayer(LAYER_INVALID); + cell->setLayer(m_layer); + classifyLayerMapCell(i+m_xOrigin, j+m_yOrigin, cell, m_bridge); + } + BridgeInfo info; + m_bridge->getBridgeInfo(&info); + Coord3D bridgeDir = info.to; + bridgeDir.x -= info.from.x; + bridgeDir.y -= info.from.y; + bridgeDir.z -= info.from.z; + bridgeDir.normalize(); + bridgeDir.x *= PATHFIND_CELL_SIZE_F*0.7f; + bridgeDir.y *= PATHFIND_CELL_SIZE_F*0.7f; + + m_startCell.x = REAL_TO_INT_FLOOR((info.from.x-bridgeDir.x) / PATHFIND_CELL_SIZE_F); + m_startCell.y = REAL_TO_INT_FLOOR((info.from.y-bridgeDir.y) / PATHFIND_CELL_SIZE_F); + m_endCell.x = REAL_TO_INT_FLOOR((info.to.x+bridgeDir.x) / PATHFIND_CELL_SIZE_F); + m_endCell.y = REAL_TO_INT_FLOOR((info.to.y+bridgeDir.y) / PATHFIND_CELL_SIZE_F); + } + if (m_destroyed) { + Int i, j; + for (i=0; igetConnectLayer() == LAYER_GROUND) { + PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i+m_xOrigin, j+m_yOrigin); + DEBUG_ASSERTCRASH(groundCell, ("Should have cell.")); + if (groundCell) { + DEBUG_ASSERTCRASH(groundCell->getConnectLayer()==m_layer, ("Should connect to this layer.jba.")); + groundCell->setConnectLayer(LAYER_INVALID); // disconnect it. + } + } +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + cell->setType(PathfindCell::CELL_IMPASSABLE); +#else + cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); +#endif + } + } + } +} + +/** + * Classifies the pathfind cells for the wall bridge layer. + */ +void PathfindLayer::classifyWallCells(ObjectID *wallPieces, Int numPieces) +{ + DEBUG_ASSERTCRASH(m_layer==LAYER_WALL, ("Wrong layer for wall.")); + if (m_layer != LAYER_WALL) return; + if (m_layerCells == nullptr) return; + + Int i, j; + for (i=0; isetConnectLayer(LAYER_INVALID); + cell->setLayer(m_layer); + classifyWallMapCell(i+m_xOrigin, j+m_yOrigin, cell, wallPieces, numPieces); + cell->setPinched(false); + } + } + if (m_destroyed) { + Int i, j; + for (i=0; igetConnectLayer() == LAYER_GROUND) { + PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i+m_xOrigin, j+m_yOrigin); + DEBUG_ASSERTCRASH(groundCell, ("Should have cell.")); + if (groundCell) { + DEBUG_ASSERTCRASH(groundCell->getConnectLayer()==m_layer, ("Should connect to this layer.jba.")); + groundCell->setConnectLayer(LAYER_INVALID); // disconnect it. + } + } + cell->setType(PathfindCell::CELL_IMPASSABLE); + } + } + } + + // Tighten up 1 cell. + for (i=1; igetType() != PathfindCell::CELL_CLEAR) { + cell->setPinched(true); + } + } + } + } + } + for (i=0; igetPinched() && cell->getType() == PathfindCell::CELL_CLEAR) { + cell->setType(PathfindCell::CELL_CLIFF); + } + cell->setPinched(false); + } + } +} + +/** + * Relassifies the pathfind cells for the destroyed bridge layer. + */ +Bool PathfindLayer::setDestroyed(Bool destroyed) +{ + if (destroyed == m_destroyed) return false; + + m_destroyed = destroyed; + classifyCells(); + + return true; +} + +/** + * Copies m_zone into the zone for all the member cells. + */ +void PathfindLayer::applyZone() +{ + Int i, j; + for (i=0; isetZone(m_zone); + } + } +} + + +/** + * Return the bridge's object id. + */ +ObjectID PathfindLayer::getBridgeID() +{ + return m_bridge->peekBridgeInfo()->bridgeObjectID; +} + +/** + * Return the cell at the index location. + */ +PathfindCell *PathfindLayer::getCell(Int x, Int y) +{ + DEBUG_ASSERTCRASH(m_layerCells, ("no data in layer, why get cells?")); + if (m_layerCells==nullptr) { + return nullptr; + } + x -= m_xOrigin; + y -= m_yOrigin; + if (x<0 || x>=m_width) return nullptr; + if (y<0 || y>=m_height) return nullptr; + PathfindCell *cell = &m_layerCells[x][y]; + if (cell->getType() == PathfindCell::CELL_IMPASSABLE) { + return nullptr; // Impassable cells are ignored. + } + return cell; +} + + +/** + * Classify the given map cell as clear, or not, etc. + */ +void PathfindLayer::classifyLayerMapCell( Int i, Int j , PathfindCell *cell, Bridge *theBridge) +{ + Coord3D topLeftCorner, bottomRightCorner; + + topLeftCorner.y = (Real)j * PATHFIND_CELL_SIZE_F; + bottomRightCorner.y = topLeftCorner.y + PATHFIND_CELL_SIZE_F; + + topLeftCorner.x = (Real)i * PATHFIND_CELL_SIZE_F; + bottomRightCorner.x = topLeftCorner.x + PATHFIND_CELL_SIZE_F; + + + Int bridgeCount = 0; + Coord3D pt; + if (theBridge->isPointOnBridge(&topLeftCorner) ) { + bridgeCount++; + } + pt = topLeftCorner; + pt.y = bottomRightCorner.y; + if (theBridge->isPointOnBridge(&pt) ) { + bridgeCount++; + } + if (theBridge->isPointOnBridge(&bottomRightCorner) ) { + bridgeCount++; + } + pt = topLeftCorner; + pt.x = bottomRightCorner.x; + if (theBridge->isPointOnBridge(&pt) ) { + bridgeCount++; + } + cell->reset(); + cell->setLayer(m_layer); + cell->setType(PathfindCell::CELL_IMPASSABLE); + if (bridgeCount == 4) { + cell->setType(PathfindCell::CELL_CLEAR); + } else { + if (bridgeCount!=0) { +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + cell->setType(PathfindCell::CELL_CLIFF); // it's off the bridge. +#else + cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); // it's off the bridge. +#endif + } + + // check against the end lines. + + Region2D cellBounds; + cellBounds.lo = topLeftCorner.asCoord2D(); + cellBounds.hi = bottomRightCorner.asCoord2D(); + +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + if (m_bridge->isCellOnEnd(&cellBounds)) { + cell->setType(PathfindCell::CELL_CLEAR); + } + if (m_bridge->isCellOnSide(&cellBounds)) { + cell->setType(PathfindCell::CELL_CLIFF); + } else { + if (m_bridge->isCellEntryPoint(&cellBounds)) { + cell->setType(PathfindCell::CELL_CLEAR); + cell->setConnectLayer(LAYER_GROUND); + PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i, j ); + groundCell->setConnectLayer(cell->getLayer()); + } + } +#else + if (m_bridge->isCellOnSide(&cellBounds)) { + cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); + } else { + if (m_bridge->isCellOnEnd(&cellBounds)) { + cell->setType(PathfindCell::CELL_CLEAR); + } + if (m_bridge->isCellEntryPoint(&cellBounds)) { + cell->setType(PathfindCell::CELL_CLEAR); + cell->setConnectLayer(LAYER_GROUND); + PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND, i, j ); + groundCell->setConnectLayer(cell->getLayer()); + } + } +#endif + } + Coord3D center = topLeftCorner; + center.x += PATHFIND_CELL_SIZE/2; + center.y += PATHFIND_CELL_SIZE/2; + if (cell->getType()!=PathfindCell::CELL_IMPASSABLE) { + if (!(cell->getConnectLayer()==LAYER_GROUND) ) { + // Check for bridge clearance. If the ground isn't 1 pathfind cells below, mark impassable. + Real groundHeight = TheTerrainLogic->getLayerHeight( center.x, center.y, LAYER_GROUND ); + Real bridgeHeight = theBridge->getBridgeHeight( ¢er, nullptr ); + if (groundHeight+LAYER_Z_CLOSE_ENOUGH_F > bridgeHeight) { + PathfindCell *groundCell = TheAI->pathfinder()->getCell(LAYER_GROUND,i, j); + if (!(groundCell->getType()==PathfindCell::CELL_OBSTACLE)) { +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + groundCell->setType(PathfindCell::CELL_IMPASSABLE); +#else + groundCell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); +#endif + } + } + } + } +} + + +Bool PathfindLayer::isPointOnWall(ObjectID *wallPieces, Int numPieces, const Coord3D *pt) +{ + Int i; + for (i=0; ifindObjectByID(wallPieces[i]); + if (obj==nullptr) continue; + Real major = obj->getGeometryInfo().getMajorRadius(); + Real minor = (obj->getGeometryInfo().getGeomType() == GEOMETRY_SPHERE) ? obj->getGeometryInfo().getMajorRadius() : obj->getGeometryInfo().getMinorRadius(); + + Real c = (Real)Cos(-obj->getOrientation()); + Real s = (Real)Sin(-obj->getOrientation()); + + // convert to a delta relative to rect ctr + Real ptx = pt->x - obj->getPosition()->x; + Real pty = pt->y - obj->getPosition()->y; + + // inverse-rotate it to the right coord system + Real ptx_new = (Real)fabs(ptx*c - pty*s); + Real pty_new = (Real)fabs(ptx*s + pty*c); + + if (ptx_new <= major && pty_new <= minor) + { + return true; + } + } + return false; +} + + +/** + * Classify the given map cell as clear, or not, etc. + */ +void PathfindLayer::classifyWallMapCell( Int i, Int j , PathfindCell *cell, ObjectID *wallPieces, Int numPieces) +{ + Coord3D topLeftCorner, bottomRightCorner; + + topLeftCorner.y = (Real)j * PATHFIND_CELL_SIZE_F; + bottomRightCorner.y = topLeftCorner.y + PATHFIND_CELL_SIZE_F; + + topLeftCorner.x = (Real)i * PATHFIND_CELL_SIZE_F; + bottomRightCorner.x = topLeftCorner.x + PATHFIND_CELL_SIZE_F; + + + Int bridgeCount = 0; + Coord3D pt; + if (isPointOnWall(wallPieces, numPieces, &topLeftCorner) ) { + bridgeCount++; + } + pt = topLeftCorner; + pt.y = bottomRightCorner.y; + if (isPointOnWall(wallPieces, numPieces, &pt) ) { + bridgeCount++; + } + if (isPointOnWall(wallPieces, numPieces, &bottomRightCorner) ) { + bridgeCount++; + } + pt = topLeftCorner; + pt.x = bottomRightCorner.x; + if (isPointOnWall(wallPieces, numPieces, &pt) ) { + bridgeCount++; + } + cell->reset(); + cell->setLayer(m_layer); + cell->setType(PathfindCell::CELL_IMPASSABLE); + if (bridgeCount == 4) { + cell->setType(PathfindCell::CELL_CLEAR); + } else { + if (bridgeCount!=0) { +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + cell->setType(PathfindCell::CELL_CLIFF); // it's off the bridge. +#else + cell->setType(PathfindCell::CELL_BRIDGE_IMPASSABLE); // it's off the bridge. +#endif + } + + } +}