1 #include "TestData.h"
2 #include "openrct2/core/StringReader.h"
3 #include "openrct2/peep/Guest.h"
4 #include "openrct2/peep/GuestPathfinding.h"
5 #include "openrct2/ride/Station.h"
6 #include "openrct2/scenario/Scenario.h"
7
8 #include <gtest/gtest.h>
9 #include <openrct2/Context.h>
10 #include <openrct2/Game.h>
11 #include <openrct2/OpenRCT2.h>
12 #include <openrct2/ParkImporter.h>
13 #include <openrct2/platform/platform.h>
14 #include <openrct2/world/Footpath.h>
15 #include <openrct2/world/Map.h>
16
17 using namespace OpenRCT2;
18
operator <<(std::ostream & os,const TileCoordsXYZ & coords)19 static std::ostream& operator<<(std::ostream& os, const TileCoordsXYZ& coords)
20 {
21 return os << "(" << coords.x << ", " << coords.y << ", " << coords.z << ")";
22 }
23
24 class PathfindingTestBase : public testing::Test
25 {
26 public:
SetUpTestCase()27 static void SetUpTestCase()
28 {
29 core_init();
30
31 gOpenRCT2Headless = true;
32 gOpenRCT2NoGraphics = true;
33 _context = CreateContext();
34 const bool initialised = _context->Initialise();
35 ASSERT_TRUE(initialised);
36
37 std::string parkPath = TestData::GetParkPath("pathfinding-tests.sv6");
38 load_from_sv6(parkPath.c_str());
39 game_load_init();
40 }
41
SetUp()42 void SetUp() override
43 {
44 // Use a consistent random seed in every test
45 scenario_rand_seed(0x12345678, 0x87654321);
46 }
47
TearDownTestCase()48 static void TearDownTestCase()
49 {
50 _context = nullptr;
51 }
52
53 protected:
FindRideByName(const char * name)54 static Ride* FindRideByName(const char* name)
55 {
56 for (auto& ride : GetRideManager())
57 {
58 auto thisName = ride.GetName();
59 if (!_strnicmp(thisName.c_str(), name, sizeof(thisName)))
60 {
61 return &ride;
62 }
63 }
64 return nullptr;
65 }
66
FindPath(TileCoordsXYZ * pos,const TileCoordsXYZ & goal,int expectedSteps,ride_id_t targetRideID)67 static bool FindPath(TileCoordsXYZ* pos, const TileCoordsXYZ& goal, int expectedSteps, ride_id_t targetRideID)
68 {
69 // Our start position is in tile coordinates, but we need to give the peep spawn
70 // position in actual world coords (32 units per tile X/Y, 8 per Z level).
71 // Add 16 so the peep spawns in the centre of the tile.
72 auto* peep = Guest::Generate(pos->ToCoordsXYZ().ToTileCentre());
73
74 // Peeps that are outside of the park use specialized pathfinding which we don't want to
75 // use here
76 peep->OutsideOfPark = false;
77
78 // An earlier iteration of this code just gave peeps a target position to walk to, but it turns out
79 // that with no actual ride to head towards, when a peep reaches a junction they use the 'aimless'
80 // pathfinder instead of pursuing their original pathfinding target. So, we always need to give them
81 // an actual ride to walk to the entrance of.
82 peep->GuestHeadingToRideId = targetRideID;
83
84 // Pick the direction the peep should initially move in, given the goal position.
85 // This will also store the goal position and initialize pathfinding data for the peep.
86 gPeepPathFindGoalPosition = goal;
87 const Direction moveDir = peep_pathfind_choose_direction(*pos, peep);
88 if (moveDir == INVALID_DIRECTION)
89 {
90 // Couldn't determine a direction to move off in
91 return false;
92 }
93
94 // We have already set up the peep's overall pathfinding goal, but we also have to set their initial
95 // 'destination' which is a close position that they will walk towards in a straight line - in this case, one
96 // tile away. Stepping the peep will move them towards their destination, and once they reach it, a new
97 // destination will be picked, to try and get the peep towards the overall pathfinding goal.
98 peep->PeepDirection = moveDir;
99 auto destination = CoordsDirectionDelta[moveDir] + peep->GetLocation();
100 peep->SetDestination(destination, 2);
101
102 // Repeatedly step the peep, until they reach the target position or until the expected number of steps have
103 // elapsed. Each step, check that the tile they are standing on is not marked as forbidden in the test data
104 // (red neon ground type).
105 int step = 0;
106 while (!(*pos == goal) && step < expectedSteps)
107 {
108 uint8_t pathingResult = 0;
109 peep->PerformNextAction(pathingResult);
110 ++step;
111
112 *pos = TileCoordsXYZ(peep->GetLocation());
113
114 EXPECT_PRED_FORMAT1(AssertIsNotForbiddenPosition, *pos);
115
116 // Check that the peep is still on a footpath. Use next_z instead of pos->z here because pos->z will change
117 // when the peep is halfway up a slope, but next_z will not change until they move to the next tile.
118 EXPECT_NE(map_get_footpath_element({ pos->ToCoordsXY(), peep->NextLoc.z }), nullptr);
119 }
120
121 // Clean up the peep, because we're reusing this loaded context for all tests.
122 peep_sprite_remove(peep);
123
124 // Require that the number of steps taken is exactly what we expected. The pathfinder is supposed to be
125 // deterministic, and we reset the RNG seed for each test, everything should be entirely repeatable; as
126 // such a change in the number of steps taken on one of these paths needs to be reviewed. For the negative
127 // tests, we will not have reached the goal but we still expect the loop to have run for the total number
128 // of steps requested before giving up.
129 EXPECT_EQ(step, expectedSteps);
130
131 return *pos == goal;
132 }
133
AssertIsStartPosition(const char *,const TileCoordsXYZ & location)134 static ::testing::AssertionResult AssertIsStartPosition(const char*, const TileCoordsXYZ& location)
135 {
136 const uint32_t expectedSurfaceStyle = 11u;
137 const uint32_t style = map_get_surface_element_at(location.ToCoordsXYZ())->GetSurfaceStyle();
138
139 if (style != expectedSurfaceStyle)
140 return ::testing::AssertionFailure()
141 << "Start location " << location << " should have surface style " << expectedSurfaceStyle
142 << " but actually has style " << style
143 << ". Either the test map is not set up correctly, or you got the coordinates wrong.";
144
145 return ::testing::AssertionSuccess();
146 }
147
AssertIsNotForbiddenPosition(const char *,const TileCoordsXYZ & location)148 static ::testing::AssertionResult AssertIsNotForbiddenPosition(const char*, const TileCoordsXYZ& location)
149 {
150 const uint32_t forbiddenSurfaceStyle = 8u;
151
152 const uint32_t style = map_get_surface_element_at(location.ToCoordsXYZ())->GetSurfaceStyle();
153
154 if (style == forbiddenSurfaceStyle)
155 return ::testing::AssertionFailure()
156 << "Path traversed location " << location << ", but it is marked as a forbidden location (surface style "
157 << forbiddenSurfaceStyle << "). Either the map is set up incorrectly, or the pathfinder went the wrong way.";
158
159 return ::testing::AssertionSuccess();
160 }
161
162 private:
163 static std::shared_ptr<IContext> _context;
164 };
165
166 std::shared_ptr<IContext> PathfindingTestBase::_context;
167
168 struct SimplePathfindingScenario
169 {
170 const char* name;
171 TileCoordsXYZ start;
172 uint32_t steps;
173
SimplePathfindingScenarioSimplePathfindingScenario174 SimplePathfindingScenario(const char* _name, const TileCoordsXYZ& _start, int _steps)
175 : name(_name)
176 , start(_start)
177 , steps(_steps)
178 {
179 }
180
ToNameSimplePathfindingScenario181 static std::string ToName(const ::testing::TestParamInfo<SimplePathfindingScenario>& param_info)
182 {
183 return param_info.param.name;
184 }
185 };
186
187 class SimplePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface<SimplePathfindingScenario>
188 {
189 };
190
TEST_P(SimplePathfindingTest,CanFindPathFromStartToGoal)191 TEST_P(SimplePathfindingTest, CanFindPathFromStartToGoal)
192 {
193 const SimplePathfindingScenario& scenario = GetParam();
194
195 ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start);
196 TileCoordsXYZ pos = scenario.start;
197
198 auto ride = FindRideByName(scenario.name);
199 ASSERT_NE(ride, nullptr);
200
201 auto entrancePos = ride_get_entrance_location(ride, 0);
202 TileCoordsXYZ goal = TileCoordsXYZ(
203 entrancePos.x - TileDirectionDelta[entrancePos.direction].x,
204 entrancePos.y - TileDirectionDelta[entrancePos.direction].y, entrancePos.z);
205
206 const auto succeeded = FindPath(&pos, goal, scenario.steps, ride->id) ? ::testing::AssertionSuccess()
207 : ::testing::AssertionFailure()
208 << "Failed to find path from " << scenario.start << " to " << goal << " in " << scenario.steps << " steps; reached "
209 << pos << " before giving up.";
210
211 EXPECT_TRUE(succeeded);
212 }
213
214 INSTANTIATE_TEST_CASE_P(
215 ForScenario, SimplePathfindingTest,
216 ::testing::Values(
217 SimplePathfindingScenario("StraightFlat", { 19, 15, 14 }, 24), SimplePathfindingScenario("SBend", { 15, 12, 14 }, 87),
218 SimplePathfindingScenario("UBend", { 17, 9, 14 }, 87), SimplePathfindingScenario("CBend", { 14, 5, 14 }, 164),
219 SimplePathfindingScenario("TwoEqualRoutes", { 9, 13, 14 }, 89),
220 SimplePathfindingScenario("TwoUnequalRoutes", { 3, 13, 14 }, 89),
221 SimplePathfindingScenario("StraightUpBridge", { 12, 15, 14 }, 24),
222 SimplePathfindingScenario("StraightUpSlope", { 14, 15, 14 }, 24),
223 SimplePathfindingScenario("SelfCrossingPath", { 6, 5, 14 }, 211)),
224 SimplePathfindingScenario::ToName);
225
226 class ImpossiblePathfindingTest : public PathfindingTestBase, public ::testing::WithParamInterface<SimplePathfindingScenario>
227 {
228 };
229
TEST_P(ImpossiblePathfindingTest,CannotFindPathFromStartToGoal)230 TEST_P(ImpossiblePathfindingTest, CannotFindPathFromStartToGoal)
231 {
232 const SimplePathfindingScenario& scenario = GetParam();
233 TileCoordsXYZ pos = scenario.start;
234 ASSERT_PRED_FORMAT1(AssertIsStartPosition, scenario.start);
235
236 auto ride = FindRideByName(scenario.name);
237 ASSERT_NE(ride, nullptr);
238
239 auto entrancePos = ride_get_entrance_location(ride, 0);
240 TileCoordsXYZ goal = TileCoordsXYZ(
241 entrancePos.x + TileDirectionDelta[entrancePos.direction].x,
242 entrancePos.y + TileDirectionDelta[entrancePos.direction].y, entrancePos.z);
243
244 EXPECT_FALSE(FindPath(&pos, goal, 10000, ride->id));
245 }
246
247 INSTANTIATE_TEST_CASE_P(
248 ForScenario, ImpossiblePathfindingTest,
249 ::testing::Values(
250 SimplePathfindingScenario("PathWithGap", { 1, 6, 14 }, 10000),
251 SimplePathfindingScenario("PathWithFences", { 11, 6, 14 }, 10000),
252 SimplePathfindingScenario("PathWithCliff", { 7, 17, 14 }, 10000)),
253 SimplePathfindingScenario::ToName);
254