From d9274deff6bf1ae04179def0b4fdcdbb3f3169d4 Mon Sep 17 00:00:00 2001 From: Stubbjax Date: Thu, 29 Jan 2026 02:13:49 +1100 Subject: [PATCH] bugfix: Resolve inaccurate circle fill logic in the partition manager --- Core/GameEngine/Include/Common/GameDefines.h | 4 ++ .../Include/GameLogic/PartitionManager.h | 5 +++ .../GameLogic/Object/PartitionManager.cpp | 43 +++++++++++++++++++ .../Include/GameLogic/PartitionManager.h | 5 +++ .../GameLogic/Object/PartitionManager.cpp | 43 +++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index 47a661a963e..f76089e34f5 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -45,6 +45,10 @@ #define RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION (1) #endif +#ifndef RETAIL_COMPATIBLE_CIRCLE_FILL_ALGORITHM +#define RETAIL_COMPATIBLE_CIRCLE_FILL_ALGORITHM (1) // Use the original circle fill algorithm, which is more efficient but less accurate +#endif + // This is essentially synonymous for RETAIL_COMPATIBLE_CRC. There is a lot wrong with AIGroup, such as use-after-free, double-free, leaks, // but we cannot touch it much without breaking retail compatibility. Do not shy away from using massive hacks when fixing issues with AIGroup, // but put them behind this macro. diff --git a/Generals/Code/GameEngine/Include/GameLogic/PartitionManager.h b/Generals/Code/GameEngine/Include/GameLogic/PartitionManager.h index e893104da1d..18e5834f31a 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/PartitionManager.h +++ b/Generals/Code/GameEngine/Include/GameLogic/PartitionManager.h @@ -463,6 +463,11 @@ class PartitionData : public MemoryPoolObject Real radius ); + /** + A more advanced implementation of doCircleFill that is 100% accurate. + */ + void doCircleFillPrecise(Real centerX, Real centerY, Real radius); + /** fill in the pixels covered by the given rectangular shape with the given center, dimensions, and rotation. diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp index 7620e751e3c..9175c4c801f 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp @@ -1878,6 +1878,43 @@ void PartitionData::doCircleFill( } } +static Bool doesCircleOverlapCell(Real centerX, Real centerY, Real radius, Real cellX, Real cellY, Real cellSize) +{ + Real closestX = maxReal(cellX, minReal(centerX, cellX + cellSize)); + Real closestY = maxReal(cellY, minReal(centerY, cellY + cellSize)); + Real distX = centerX - closestX; + Real distY = centerY - closestY; + + return (distX * distX + distY * distY) < radius * radius; +} + +void PartitionData::doCircleFillPrecise(Real centerX, Real centerY, Real radius) +{ + Int minCellX, minCellY, maxCellX, maxCellY; + ThePartitionManager->worldToCell(centerX - radius, centerY - radius, &minCellX, &minCellY); + ThePartitionManager->worldToCell(centerX + radius, centerY + radius, &maxCellX, &maxCellY); + + Real cellSize = ThePartitionManager->getCellSize(); + + for (Int x = minCellX; x <= maxCellX; ++x) + { + for (Int y = minCellY; y <= maxCellY; ++y) + { + Real cellWorldX = x * cellSize; + Real cellWorldY = y * cellSize; + + if (doesCircleOverlapCell(centerX, centerY, radius, cellWorldX, cellWorldY, cellSize)) + { + PartitionCell* cell = ThePartitionManager->getCellAt(x, y); + if (cell) + { + addSubPixToCoverage(cell); + } + } + } + } +} + // ----------------------------------------------------------------------------- void PartitionData::doSmallFill( Real centerX, @@ -2079,7 +2116,13 @@ void PartitionData::updateCellsTouched() case GEOMETRY_SPHERE: case GEOMETRY_CYLINDER: { +#if RETAIL_COMPATIBLE_CRC || RETAIL_COMPATIBLE_CIRCLE_FILL_ALGORITHM doCircleFill(pos.x, pos.y, majorRadius); +#else + // TheSuperHackers @bugfix Stubbjax 29/01/2026 Use precise circle fill to improve + // collision accuracy, most notably for objects with geometry radii >= 20 and < 40. + doCircleFillPrecise(pos.x, pos.y, majorRadius); +#endif break; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/PartitionManager.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/PartitionManager.h index 365af131ed7..36f93f2a17e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/PartitionManager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/PartitionManager.h @@ -464,6 +464,11 @@ class PartitionData : public MemoryPoolObject Real radius ); + /** + A more advanced implementation of doCircleFill that is 100% accurate. + */ + void doCircleFillPrecise(Real centerX, Real centerY, Real radius); + /** fill in the pixels covered by the given rectangular shape with the given center, dimensions, and rotation. diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp index 03d9bf6b746..5bed512200b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp @@ -1882,6 +1882,43 @@ void PartitionData::doCircleFill( } } +static Bool doesCircleOverlapCell(Real centerX, Real centerY, Real radius, Real cellX, Real cellY, Real cellSize) +{ + Real closestX = maxReal(cellX, minReal(centerX, cellX + cellSize)); + Real closestY = maxReal(cellY, minReal(centerY, cellY + cellSize)); + Real distX = centerX - closestX; + Real distY = centerY - closestY; + + return (distX * distX + distY * distY) < radius * radius; +} + +void PartitionData::doCircleFillPrecise(Real centerX, Real centerY, Real radius) +{ + Int minCellX, minCellY, maxCellX, maxCellY; + ThePartitionManager->worldToCell(centerX - radius, centerY - radius, &minCellX, &minCellY); + ThePartitionManager->worldToCell(centerX + radius, centerY + radius, &maxCellX, &maxCellY); + + Real cellSize = ThePartitionManager->getCellSize(); + + for (Int x = minCellX; x <= maxCellX; ++x) + { + for (Int y = minCellY; y <= maxCellY; ++y) + { + Real cellWorldX = x * cellSize; + Real cellWorldY = y * cellSize; + + if (doesCircleOverlapCell(centerX, centerY, radius, cellWorldX, cellWorldY, cellSize)) + { + PartitionCell* cell = ThePartitionManager->getCellAt(x, y); + if (cell) + { + addSubPixToCoverage(cell); + } + } + } + } +} + // ----------------------------------------------------------------------------- void PartitionData::doSmallFill( Real centerX, @@ -2083,7 +2120,13 @@ void PartitionData::updateCellsTouched() case GEOMETRY_SPHERE: case GEOMETRY_CYLINDER: { +#if RETAIL_COMPATIBLE_CRC || RETAIL_COMPATIBLE_CIRCLE_FILL_ALGORITHM doCircleFill(pos.x, pos.y, majorRadius); +#else + // TheSuperHackers @bugfix Stubbjax 29/01/2026 Use precise circle fill to improve + // collision accuracy, most notably for objects with geometry radii >= 20 and < 40. + doCircleFillPrecise(pos.x, pos.y, majorRadius); +#endif break; }