From b55f17931305056b14763d8aa07f73090ad64e78 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 16:34:25 +0200 Subject: [PATCH 1/3] Account for static blockers in frontier reachability --- .../s25main/pathfinding/FindPathReachable.cpp | 21 ++++++++++- .../integration/testFrontierDistance.cpp | 37 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/libs/s25main/pathfinding/FindPathReachable.cpp b/libs/s25main/pathfinding/FindPathReachable.cpp index 212eab420a..53baca3d61 100644 --- a/libs/s25main/pathfinding/FindPathReachable.cpp +++ b/libs/s25main/pathfinding/FindPathReachable.cpp @@ -6,10 +6,29 @@ #include "pathfinding/FreePathFinderImpl.h" #include "pathfinding/PathConditionReachable.h" #include "world/GameWorldBase.h" +#include "nodeObjs/noBase.h" + +namespace { + +struct PathConditionReachableWithStaticBlockers : PathConditionReachable +{ + PathConditionReachableWithStaticBlockers(const World& world) : PathConditionReachable(world) {} + + BOOST_FORCEINLINE bool IsNodeOk(const MapPoint& pt) const + { + const auto* obj = world.GetNode(pt).obj; + if(obj && obj->GetGOT() == GO_Type::Staticobject && obj->GetBM() != BlockingManner::None) + return false; + + return PathConditionReachable::IsNodeOk(pt); + } +}; + +} // namespace bool DoesReachablePathExist(const GameWorldBase& world, const MapPoint startPt, const MapPoint endPt, unsigned maxLen) { RTTR_Assert(startPt != endPt); return world.GetFreePathFinder().FindPath(startPt, endPt, false, maxLen, nullptr, nullptr, nullptr, - PathConditionReachable(world)); + PathConditionReachableWithStaticBlockers(world)); } diff --git a/tests/s25Main/integration/testFrontierDistance.cpp b/tests/s25Main/integration/testFrontierDistance.cpp index 8435e05270..82ae9a307f 100644 --- a/tests/s25Main/integration/testFrontierDistance.cpp +++ b/tests/s25Main/integration/testFrontierDistance.cpp @@ -11,6 +11,8 @@ #include "worldFixtures/WorldFixture.h" #include "worldFixtures/terrainHelpers.h" #include "world/MapLoader.h" +#include "pathfinding/FindPathReachable.h" +#include "nodeObjs/noStaticObject.h" #include #include @@ -56,6 +58,7 @@ using FrontierWorldSmall = FrontierWorld<34u, 20u>; using FrontierWorldMiddle = FrontierWorld<38u, 20u>; using FrontierWorldBig = FrontierWorld<60u, 20u>; using FrontierWorldSea = FrontierWorld::width, SmallSeaWorldDefault<2>::height, CreateSeaWorld>; +using FrontierWorldStaticBlocker = WorldFixture; } // namespace @@ -347,4 +350,38 @@ BOOST_FIXTURE_TEST_CASE(FrontierDistanceBug_815, WorldBig) BOOST_TEST_REQUIRE(distance1 == FrontierDistance::Near); } + +BOOST_FIXTURE_TEST_CASE(FrontierDistanceReachabilityConsidersStaticObjectBlockers, FrontierWorldStaticBlocker) +{ + const DescIdx tWater = GetWaterTerrain(world.GetDescription()); + const unsigned corridorY = 6; + const MapPoint startPt(2, corridorY); + const MapPoint endPt(10, corridorY); + constexpr unsigned maxPathLen = 8; + + for(const auto y : helpers::range(0, +world.GetHeight())) + { + for(const auto x : helpers::range(0, +world.GetWidth())) + { + const MapPoint pt(x, y); + if(pt.y == corridorY && pt.x >= startPt.x && pt.x <= endPt.x) + continue; + + MapNode& node = world.GetNodeWriteable(pt); + node.t1 = tWater; + node.t2 = tWater; + } + } + + BOOST_TEST_REQUIRE(DoesReachablePathExist(world, startPt, endPt, maxPathLen)); + + for(unsigned x = startPt.x + 1; x < endPt.x; ++x) + { + const MapPoint blockerPt(x, corridorY); + world.DestroyNO(blockerPt, false); + world.SetNO(blockerPt, new noStaticObject(blockerPt, 0, 0, 1)); + } + + BOOST_TEST_REQUIRE(!DoesReachablePathExist(world, startPt, endPt, maxPathLen)); +} BOOST_AUTO_TEST_SUITE_END() From 5b074798c85936b96c0cbaa277a54e3923196153 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 2 May 2026 13:42:42 +0200 Subject: [PATCH 2/3] Fix frontier distance test formatting --- .../integration/testFrontierDistance.cpp | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/s25Main/integration/testFrontierDistance.cpp b/tests/s25Main/integration/testFrontierDistance.cpp index 82ae9a307f..4891f24f3d 100644 --- a/tests/s25Main/integration/testFrontierDistance.cpp +++ b/tests/s25Main/integration/testFrontierDistance.cpp @@ -6,12 +6,13 @@ #include "buildings/nobMilitary.h" #include "factories/BuildingFactory.h" #include "helpers/Range.h" +#include "pathfinding/FindPathReachable.h" #include "worldFixtures/CreateEmptyWorld.h" #include "worldFixtures/CreateSeaWorld.h" #include "worldFixtures/WorldFixture.h" #include "worldFixtures/terrainHelpers.h" #include "world/MapLoader.h" -#include "pathfinding/FindPathReachable.h" +#include "nodeObjs/noEnvObject.h" #include "nodeObjs/noStaticObject.h" #include #include @@ -350,7 +351,6 @@ BOOST_FIXTURE_TEST_CASE(FrontierDistanceBug_815, WorldBig) BOOST_TEST_REQUIRE(distance1 == FrontierDistance::Near); } - BOOST_FIXTURE_TEST_CASE(FrontierDistanceReachabilityConsidersStaticObjectBlockers, FrontierWorldStaticBlocker) { const DescIdx tWater = GetWaterTerrain(world.GetDescription()); @@ -375,6 +375,24 @@ BOOST_FIXTURE_TEST_CASE(FrontierDistanceReachabilityConsidersStaticObjectBlocker BOOST_TEST_REQUIRE(DoesReachablePathExist(world, startPt, endPt, maxPathLen)); + for(unsigned x = startPt.x + 1; x < endPt.x; ++x) + { + const MapPoint passablePt(x, corridorY); + world.DestroyNO(passablePt, false); + world.SetNO(passablePt, new noStaticObject(passablePt, 0, 0, 0)); + } + + BOOST_TEST_REQUIRE(DoesReachablePathExist(world, startPt, endPt, maxPathLen)); + + for(unsigned x = startPt.x + 1; x < endPt.x; ++x) + { + const MapPoint passablePt(x, corridorY); + world.DestroyNO(passablePt, false); + world.SetNO(passablePt, new noEnvObject(passablePt, 0, 0)); + } + + BOOST_TEST_REQUIRE(DoesReachablePathExist(world, startPt, endPt, maxPathLen)); + for(unsigned x = startPt.x + 1; x < endPt.x; ++x) { const MapPoint blockerPt(x, corridorY); From e59dcfbae84182a5c4f8d55525037d8177f29ff7 Mon Sep 17 00:00:00 2001 From: Manu Date: Sun, 10 May 2026 16:47:49 +0200 Subject: [PATCH 3/3] Address frontier distance review comments --- .../s25main/pathfinding/FindPathReachable.cpp | 2 ++ .../integration/testFrontierDistance.cpp | 23 ++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/libs/s25main/pathfinding/FindPathReachable.cpp b/libs/s25main/pathfinding/FindPathReachable.cpp index 53baca3d61..cebdf0af2f 100644 --- a/libs/s25main/pathfinding/FindPathReachable.cpp +++ b/libs/s25main/pathfinding/FindPathReachable.cpp @@ -10,6 +10,8 @@ namespace { +// Keep this narrower than PathConditionHuman, but reject permanently blocking static objects: +// Human pathing can never accept them and they cannot be removed. struct PathConditionReachableWithStaticBlockers : PathConditionReachable { PathConditionReachableWithStaticBlockers(const World& world) : PathConditionReachable(world) {} diff --git a/tests/s25Main/integration/testFrontierDistance.cpp b/tests/s25Main/integration/testFrontierDistance.cpp index 4891f24f3d..c35d2029bb 100644 --- a/tests/s25Main/integration/testFrontierDistance.cpp +++ b/tests/s25Main/integration/testFrontierDistance.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "GamePlayer.h" +#include "RttrForeachPt.h" #include "buildings/nobMilitary.h" #include "factories/BuildingFactory.h" #include "helpers/Range.h" @@ -359,23 +360,19 @@ BOOST_FIXTURE_TEST_CASE(FrontierDistanceReachabilityConsidersStaticObjectBlocker const MapPoint endPt(10, corridorY); constexpr unsigned maxPathLen = 8; - for(const auto y : helpers::range(0, +world.GetHeight())) + RTTR_FOREACH_PT(MapPoint, world.GetSize()) { - for(const auto x : helpers::range(0, +world.GetWidth())) - { - const MapPoint pt(x, y); - if(pt.y == corridorY && pt.x >= startPt.x && pt.x <= endPt.x) - continue; + if(pt.y == corridorY && pt.x >= startPt.x && pt.x <= endPt.x) + continue; - MapNode& node = world.GetNodeWriteable(pt); - node.t1 = tWater; - node.t2 = tWater; - } + MapNode& node = world.GetNodeWriteable(pt); + node.t1 = tWater; + node.t2 = tWater; } BOOST_TEST_REQUIRE(DoesReachablePathExist(world, startPt, endPt, maxPathLen)); - for(unsigned x = startPt.x + 1; x < endPt.x; ++x) + for(const unsigned x : helpers::range(startPt.x + 1, endPt.x)) { const MapPoint passablePt(x, corridorY); world.DestroyNO(passablePt, false); @@ -384,7 +381,7 @@ BOOST_FIXTURE_TEST_CASE(FrontierDistanceReachabilityConsidersStaticObjectBlocker BOOST_TEST_REQUIRE(DoesReachablePathExist(world, startPt, endPt, maxPathLen)); - for(unsigned x = startPt.x + 1; x < endPt.x; ++x) + for(const unsigned x : helpers::range(startPt.x + 1, endPt.x)) { const MapPoint passablePt(x, corridorY); world.DestroyNO(passablePt, false); @@ -393,7 +390,7 @@ BOOST_FIXTURE_TEST_CASE(FrontierDistanceReachabilityConsidersStaticObjectBlocker BOOST_TEST_REQUIRE(DoesReachablePathExist(world, startPt, endPt, maxPathLen)); - for(unsigned x = startPt.x + 1; x < endPt.x; ++x) + for(const unsigned x : helpers::range(startPt.x + 1, endPt.x)) { const MapPoint blockerPt(x, corridorY); world.DestroyNO(blockerPt, false);