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
+ }
+
+ }
+}