1 /*****************************************************************************
2  * Copyright (c) 2014-2018 OpenRCT2 developers
3  *
4  * For a complete list of all authors, please refer to contributors.md
5  * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
6  *
7  * OpenRCT2 is licensed under the GNU General Public License version 3.
8  *****************************************************************************/
9 
10 #include "TestTrack.hpp"
11 
12 #include "../../src/openrct2/ride/RideData.h"
13 #include "Data.h"
14 #include "FunctionCall.hpp"
15 #include "GeneralSupportHeightCall.hpp"
16 #include "PaintIntercept.hpp"
17 #include "Printer.hpp"
18 #include "SegmentSupportHeightCall.hpp"
19 #include "SideTunnelCall.hpp"
20 #include "String.hpp"
21 #include "TestPaint.hpp"
22 #include "Utils.hpp"
23 #include "VerticalTunnelCall.hpp"
24 
25 #include <openrct2/paint/Supports.h>
26 #include <openrct2/paint/tile_element/Paint.TileElement.h>
27 #include <openrct2/ride/Ride.h>
28 #include <openrct2/ride/Track.h>
29 #include <openrct2/ride/TrackData.h>
30 #include <string>
31 #include <vector>
32 
33 struct ITestTrackFilter
34 {
35 public:
~ITestTrackFilterITestTrackFilter36     virtual ~ITestTrackFilter()
37     {
38     }
39 
40     virtual bool AppliesTo(uint8_t rideType, uint8_t trackType) abstract;
41 
42     virtual int Variations(uint8_t rideType, uint8_t trackType) abstract;
43 
44     virtual std::string VariantName(uint8_t rideType, uint8_t trackType, int variant) abstract;
45 
46     virtual void ApplyTo(
47         uint8_t rideType, uint8_t trackType, int variant, TileElement* tileElement, TileElement* surfaceElement, Ride* ride,
48         rct_ride_entry* rideEntry) abstract;
49 };
50 
51 class CableLiftFilter : public ITestTrackFilter
52 {
53 public:
AppliesTo(uint8_t rideType,uint8_t trackType)54     bool AppliesTo(uint8_t rideType, uint8_t trackType) override
55     {
56         return rideType == RIDE_TYPE_GIGA_COASTER;
57     }
58 
Variations(uint8_t rideType,uint8_t trackType)59     int Variations(uint8_t rideType, uint8_t trackType) override
60     {
61         return 2;
62     }
63 
VariantName(uint8_t rideType,uint8_t trackType,int variant)64     std::string VariantName(uint8_t rideType, uint8_t trackType, int variant) override
65     {
66         return String::Format("cableLift:%d", variant);
67     }
68 
ApplyTo(uint8_t rideType,uint8_t trackType,int variant,TileElement * tileElement,TileElement * surfaceElement,Ride * ride,rct_ride_entry * rideEntry)69     virtual void ApplyTo(
70         uint8_t rideType, uint8_t trackType, int variant, TileElement* tileElement, TileElement* surfaceElement, Ride* ride,
71         rct_ride_entry* rideEntry) override
72     {
73         if (variant == 0)
74         {
75             tileElement->AsTrack()->SetHasCableLift(false);
76         }
77         else
78         {
79             tileElement->AsTrack()->SetHasCableLift(true);
80         }
81     }
82 };
83 
84 class ChainLiftFilter : public ITestTrackFilter
85 {
86 public:
AppliesTo(uint8_t rideType,uint8_t trackType)87     bool AppliesTo(uint8_t rideType, uint8_t trackType) override
88     {
89         return !GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_FLAT_RIDE);
90     }
91 
Variations(uint8_t rideType,uint8_t trackType)92     int Variations(uint8_t rideType, uint8_t trackType) override
93     {
94         return 2;
95     }
96 
VariantName(uint8_t rideType,uint8_t trackType,int variant)97     std::string VariantName(uint8_t rideType, uint8_t trackType, int variant) override
98     {
99         return String::Format("chainLift:%d", variant);
100     }
101 
ApplyTo(uint8_t rideType,uint8_t trackType,int variant,TileElement * tileElement,TileElement * surfaceElement,Ride * ride,rct_ride_entry * rideEntry)102     virtual void ApplyTo(
103         uint8_t rideType, uint8_t trackType, int variant, TileElement* tileElement, TileElement* surfaceElement, Ride* ride,
104         rct_ride_entry* rideEntry) override
105     {
106         tileElement->AsTrack()->SetHasChain(variant != 0);
107     }
108 };
109 
110 class InvertedFilter : public ITestTrackFilter
111 {
112 public:
AppliesTo(uint8_t rideType,uint8_t trackType)113     bool AppliesTo(uint8_t rideType, uint8_t trackType) override
114     {
115         if (rideType == RIDE_TYPE_MULTI_DIMENSION_ROLLER_COASTER || rideType == RIDE_TYPE_FLYING_ROLLER_COASTER
116             || rideType == RIDE_TYPE_LAY_DOWN_ROLLER_COASTER)
117         {
118             return true;
119         }
120 
121         return false;
122     }
123 
Variations(uint8_t rideType,uint8_t trackType)124     int Variations(uint8_t rideType, uint8_t trackType) override
125     {
126         return 2;
127     }
128 
VariantName(uint8_t rideType,uint8_t trackType,int variant)129     std::string VariantName(uint8_t rideType, uint8_t trackType, int variant) override
130     {
131         return String::Format("inverted:%d", variant);
132     }
133 
ApplyTo(uint8_t rideType,uint8_t trackType,int variant,TileElement * tileElement,TileElement * surfaceElement,Ride * ride,rct_ride_entry * rideEntry)134     virtual void ApplyTo(
135         uint8_t rideType, uint8_t trackType, int variant, TileElement* tileElement, TileElement* surfaceElement, Ride* ride,
136         rct_ride_entry* rideEntry) override
137     {
138         if (variant == 0)
139         {
140             tileElement->AsTrack()->SetHasCableLift(false);
141         }
142         else
143         {
144             tileElement->AsTrack()->SetHasCableLift(true);
145         }
146     }
147 };
148 
149 class EntranceStyleFilter : public ITestTrackFilter
150 {
151 public:
AppliesTo(uint8_t rideType,uint8_t trackType)152     bool AppliesTo(uint8_t rideType, uint8_t trackType) override
153     {
154         if (track_type_is_station(trackType))
155         {
156             return true;
157         }
158 
159         return false;
160     }
161 
Variations(uint8_t rideType,uint8_t trackType)162     int Variations(uint8_t rideType, uint8_t trackType) override
163     {
164         return 12;
165     }
166 
VariantName(uint8_t rideType,uint8_t trackType,int variant)167     std::string VariantName(uint8_t rideType, uint8_t trackType, int variant) override
168     {
169         return String::Format("entranceStyle:%d", variant);
170     }
171 
ApplyTo(uint8_t rideType,uint8_t trackType,int variant,TileElement * tileElement,TileElement * surfaceElement,Ride * ride,rct_ride_entry * rideEntry)172     virtual void ApplyTo(
173         uint8_t rideType, uint8_t trackType, int variant, TileElement* tileElement, TileElement* surfaceElement, Ride* ride,
174         rct_ride_entry* rideEntry) override
175     {
176         ride->entrance_style = variant;
177         RCT2_Rides[0].entrance_style = variant;
178     }
179 };
180 
CallOriginal(uint8_t rideType,uint8_t trackType,uint8_t direction,uint8_t trackSequence,uint16_t height,TileElement * tileElement)181 static void CallOriginal(
182     uint8_t rideType, uint8_t trackType, uint8_t direction, uint8_t trackSequence, uint16_t height, TileElement* tileElement)
183 {
184     uint32_t* trackDirectionList = (uint32_t*)RideTypeTrackPaintFunctionsOld[rideType][trackType];
185     const uint8_t rideIndex = 0;
186 
187     // Have to call from this point as it pushes esi and expects callee to pop it
188     RCT2_CALLPROC_X(
189         0x006C4934, rideType, (int)trackDirectionList, direction, height, (int)tileElement, rideIndex * sizeof(Ride),
190         trackSequence);
191 }
192 
CallNew(uint8_t rideType,uint8_t trackType,uint8_t direction,uint8_t trackSequence,uint16_t height,TileElement * tileElement)193 static void CallNew(
194     uint8_t rideType, uint8_t trackType, uint8_t direction, uint8_t trackSequence, uint16_t height, TileElement* tileElement)
195 {
196     TRACK_PAINT_FUNCTION_GETTER newPaintFunctionGetter = GetRideTypeDescriptor(rideType).TrackPaintFunction;
197     TRACK_PAINT_FUNCTION newPaintFunction = newPaintFunctionGetter(trackType);
198 
199     newPaintFunction(&gPaintSession, 0, trackSequence, direction, height, tileElement);
200 }
201 
202 using TestFunction = uint8_t (*)(uint8_t, uint8_t, uint8_t, std::string*);
203 
204 static uint8_t TestTrackElementPaintCalls(uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error);
205 
206 static uint8_t TestTrackElementSegmentSupportHeight(
207     uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error);
208 
209 static uint8_t TestTrackElementGeneralSupportHeight(
210     uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error);
211 
212 static uint8_t TestTrackElementSideTunnels(uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error);
213 
214 static uint8_t TestTrackElementVerticalTunnels(uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error);
215 
TestPaintTrackElement(uint8_t rideType,uint8_t trackType,std::string * out)216 uint8_t TestTrack::TestPaintTrackElement(uint8_t rideType, uint8_t trackType, std::string* out)
217 {
218     if (!Utils::rideSupportsTrackType(rideType, trackType))
219     {
220         return TEST_FAILED;
221     }
222 
223     if (rideType == RIDE_TYPE_CHAIRLIFT)
224     {
225         if (track_type_is_station(trackType))
226         {
227             // These rides check neighbouring tiles for tracks
228             return TEST_SKIPPED;
229         }
230     }
231 
232     int sequenceCount = Utils::getTrackSequenceCount(rideType, trackType);
233     std::string error = String::Format("rct2: 0x%08X\n", RideTypeTrackPaintFunctionsOld[rideType][trackType]);
234 
235     uint8_t retVal = TEST_SUCCESS;
236 
237     static TestFunction functions[] = {
238         TestTrackElementPaintCalls,  TestTrackElementSegmentSupportHeight, TestTrackElementGeneralSupportHeight,
239         TestTrackElementSideTunnels, TestTrackElementVerticalTunnels,
240     };
241 
242     for (int trackSequence = 0; trackSequence < sequenceCount; trackSequence++)
243     {
244         for (auto&& function : functions)
245         {
246             retVal = function(rideType, trackType, trackSequence, &error);
247 
248             if (retVal != TEST_SUCCESS)
249             {
250                 *out += error + "\n";
251                 return retVal;
252             }
253         }
254     }
255 
256     return retVal;
257 }
258 
TestTrackElementPaintCalls(uint8_t rideType,uint8_t trackType,uint8_t trackSequence,std::string * error)259 static uint8_t TestTrackElementPaintCalls(uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error)
260 {
261     uint16_t height = 3 * 16;
262 
263     TileElement tileElement = {};
264     tileElement.SetType(TILE_ELEMENT_TYPE_TRACK);
265     tileElement.SetLastForTile(true);
266     tileElement.AsTrack()->SetTrackType(trackType);
267     tileElement.base_height = height / 16;
268     g_currently_drawn_item = &tileElement;
269 
270     TileElement surfaceElement = {};
271     surfaceElement.SetType(TILE_ELEMENT_TYPE_SURFACE);
272     surfaceElement.base_height = MINIMUM_LAND_HEIGHT;
273     gSurfaceElement = &surfaceElement;
274     gDidPassSurface = true;
275 
276     gPaintSession.CurrentlyDrawnItem = &tileElement;
277     gPaintSession.SurfaceElement = &surfaceElement;
278     gPaintSession.DidPassSurface = true;
279 
280     TestPaint::ResetEnvironment();
281     TestPaint::ResetTunnels();
282 
283     function_call callBuffer[256] = {};
284     int callCount = 0;
285 
286     // TODO: test supports
287     // TODO: test flat rides
288     // TODO: test mazes
289     // TODO: test underground (Wooden RC)
290     // TODO: test station fences
291 
292     std::vector<ITestTrackFilter*> filters;
293     filters.push_back(new CableLiftFilter());
294     filters.push_back(new ChainLiftFilter());
295     filters.push_back(new InvertedFilter());
296     filters.push_back(new EntranceStyleFilter());
297 
298     std::vector<ITestTrackFilter*> activeFilters;
299 
300     for (auto&& filter : filters)
301     {
302         if (filter->AppliesTo(rideType, trackType))
303         {
304             activeFilters.push_back(filter);
305         }
306     }
307 
308     // Add an element so there's always something to add to
309     std::vector<uint8_t> filler;
310     filler.push_back(0);
311 
312     std::vector<std::vector<uint8_t>> argumentPermutations;
313     argumentPermutations.push_back(filler);
314     for (size_t filterIndex = 0; filterIndex < activeFilters.size(); ++filterIndex)
315     {
316         ITestTrackFilter* filter = activeFilters[filterIndex];
317         uint8_t variantCount = filter->Variations(rideType, trackType);
318 
319         std::vector<std::vector<uint8_t>> newArgumentPermutations;
320         for (int variant = 0; variant < variantCount; variant++)
321         {
322             for (auto&& oldPermutation : argumentPermutations)
323             {
324                 std::vector<uint8_t> permutation;
325                 permutation.insert(permutation.begin(), oldPermutation.begin(), oldPermutation.end());
326                 permutation.push_back(variant);
327                 newArgumentPermutations.push_back(permutation);
328             }
329         }
330 
331         argumentPermutations.clear();
332         argumentPermutations.insert(
333             argumentPermutations.begin(), newArgumentPermutations.begin(), newArgumentPermutations.end());
334     }
335 
336     for (auto&& arguments : argumentPermutations)
337     {
338         std::string baseCaseName = "[";
339 
340         for (size_t filterIndex = 0; filterIndex < activeFilters.size(); ++filterIndex)
341         {
342             uint8_t& variant = arguments[1 + filterIndex];
343             baseCaseName += activeFilters[filterIndex]->VariantName(rideType, trackType, variant);
344             baseCaseName += " ";
345 
346             activeFilters[filterIndex]->ApplyTo(
347                 rideType, trackType, variant, &tileElement, &surfaceElement, &(gRideList[0]), gRideEntries[0]);
348         }
349 
350         for (int currentRotation = 0; currentRotation < 4; currentRotation++)
351         {
352             gCurrentRotation = currentRotation;
353             RCT2_CurrentRotation = currentRotation;
354             gPaintSession.CurrentRotation = currentRotation;
355             for (int direction = 0; direction < 4; direction++)
356             {
357                 RCT2_GLOBAL(0x009DE56A, int16_t) = 64; // x
358                 RCT2_GLOBAL(0x009DE56E, int16_t) = 64; // y
359 
360                 std::string caseName = String::Format(
361                     "%srotation:%d direction:%d trackSequence:%d]", baseCaseName.c_str(), currentRotation, direction,
362                     trackSequence);
363 
364                 PaintIntercept::ClearCalls();
365                 TestPaint::ResetSupportHeights();
366                 gWoodenSupportsPrependTo = nullptr;
367 
368                 CallOriginal(rideType, trackType, direction, trackSequence, height, &tileElement);
369 
370                 callCount = PaintIntercept::GetCalls(callBuffer);
371                 std::vector<function_call> oldCalls;
372                 oldCalls.insert(oldCalls.begin(), callBuffer, callBuffer + callCount);
373 
374                 PaintIntercept::ClearCalls();
375                 TestPaint::testClearIgnore();
376                 TestPaint::ResetSupportHeights();
377                 gPaintSession.WoodenSupportsPrependTo = nullptr;
378 
379                 CallNew(rideType, trackType, direction, trackSequence, height, &tileElement);
380 
381                 if (TestPaint::testIsIgnored(direction, trackSequence))
382                 {
383                     *error += String::Format("[  IGNORED ]   %s\n", caseName.c_str());
384                     continue;
385                 }
386 
387                 callCount = PaintIntercept::GetCalls(callBuffer);
388                 std::vector<function_call> newCalls;
389                 newCalls.insert(newCalls.begin(), callBuffer, callBuffer + callCount);
390 
391                 bool success = true;
392                 if (oldCalls.size() != newCalls.size())
393                 {
394                     *error += String::Format(
395                         "Call counts don't match (was %d, expected %d). %s\n", newCalls.size(), oldCalls.size(),
396                         caseName.c_str());
397                     success = false;
398                 }
399                 else if (!FunctionCall::AssertsEquals(oldCalls, newCalls))
400                 {
401                     *error += String::Format("Calls don't match. %s\n", caseName.c_str());
402                     success = false;
403                 }
404 
405                 if (!success)
406                 {
407                     *error += " Expected:\n";
408                     *error += Printer::PrintFunctionCalls(oldCalls, height);
409                     *error += "   Actual:\n";
410                     *error += Printer::PrintFunctionCalls(newCalls, height);
411 
412                     return TEST_FAILED;
413                 }
414             }
415         }
416     }
417 
418     return TEST_SUCCESS;
419 }
420 
TestTrackElementSegmentSupportHeight(uint8_t rideType,uint8_t trackType,uint8_t trackSequence,std::string * error)421 static uint8_t TestTrackElementSegmentSupportHeight(
422     uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error)
423 {
424     uint16_t height = 3 * 16;
425 
426     TileElement tileElement = {};
427     tileElement.SetType(TILE_ELEMENT_TYPE_TRACK);
428     tileElement.SetLastForTile(true);
429     tileElement.AsTrack()->SetTrackType(trackType);
430     tileElement.base_height = height / 16;
431     g_currently_drawn_item = &tileElement;
432 
433     TileElement surfaceElement = {};
434     surfaceElement.SetType(TILE_ELEMENT_TYPE_SURFACE);
435     surfaceElement.base_height = MINIMUM_LAND_HEIGHT;
436     gSurfaceElement = &surfaceElement;
437     gDidPassSurface = true;
438 
439     gPaintSession.CurrentlyDrawnItem = &tileElement;
440     gPaintSession.SurfaceElement = &surfaceElement;
441     gPaintSession.DidPassSurface = true;
442 
443     TestPaint::ResetEnvironment();
444     TestPaint::ResetTunnels();
445 
446     // TODO: Test Chainlift
447     // TODO: Test Maze
448     // TODO: Allow skip
449 
450     std::string state = String::Format("[trackSequence:%d chainLift:%d]", trackSequence, 0);
451 
452     std::vector<SegmentSupportCall> tileSegmentSupportCalls[4];
453 
454     for (int direction = 0; direction < 4; direction++)
455     {
456         TestPaint::ResetSupportHeights();
457 
458         CallOriginal(rideType, trackType, direction, trackSequence, height, &tileElement);
459 
460         tileSegmentSupportCalls[direction] = SegmentSupportHeightCall::getSegmentCalls(gSupportSegments, direction);
461     }
462 
463     std::vector<SegmentSupportCall> referenceCalls = tileSegmentSupportCalls[0];
464 
465     if (!SegmentSupportHeightCall::CallsMatch(tileSegmentSupportCalls))
466     {
467         bool success = SegmentSupportHeightCall::FindMostCommonSupportCall(tileSegmentSupportCalls, &referenceCalls);
468         if (!success)
469         {
470             *error += String::Format("Original segment calls didn't match. %s\n", state.c_str());
471             for (int direction = 0; direction < 4; direction++)
472             {
473                 *error += String::Format("# %d\n", direction);
474                 *error += Printer::PrintSegmentSupportHeightCalls(tileSegmentSupportCalls[direction]);
475             }
476             return TEST_FAILED;
477         }
478     }
479 
480     for (int direction = 0; direction < 4; direction++)
481     {
482         TestPaint::ResetSupportHeights();
483 
484         TestPaint::testClearIgnore();
485         CallNew(rideType, trackType, direction, trackSequence, height, &tileElement);
486         if (TestPaint::testIsIgnored(direction, trackSequence))
487         {
488             continue;
489         }
490 
491         std::vector<SegmentSupportCall> newCalls = SegmentSupportHeightCall::getSegmentCalls(
492             gPaintSession.SupportSegments, direction);
493         if (!SegmentSupportHeightCall::CallsEqual(referenceCalls, newCalls))
494         {
495             *error += String::Format("Segment support heights didn't match. [direction:%d] %s\n", direction, state.c_str());
496             *error += " Expected:\n";
497             *error += Printer::PrintSegmentSupportHeightCalls(referenceCalls);
498             *error += "   Actual:\n";
499             *error += Printer::PrintSegmentSupportHeightCalls(newCalls);
500 
501             return TEST_FAILED;
502         }
503     }
504 
505     return TEST_SUCCESS;
506 }
507 
TestTrackElementGeneralSupportHeight(uint8_t rideType,uint8_t trackType,uint8_t trackSequence,std::string * error)508 static uint8_t TestTrackElementGeneralSupportHeight(
509     uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error)
510 {
511     uint16_t height = 3 * 16;
512 
513     TileElement tileElement = {};
514     tileElement.SetType(TILE_ELEMENT_TYPE_TRACK);
515     tileElement.SetLastForTile(true);
516     tileElement.AsTrack()->SetTrackType(trackType);
517     tileElement.base_height = height / 16;
518     g_currently_drawn_item = &tileElement;
519 
520     TileElement surfaceElement = {};
521     surfaceElement.SetType(TILE_ELEMENT_TYPE_SURFACE);
522     surfaceElement.base_height = MINIMUM_LAND_HEIGHT;
523     gSurfaceElement = &surfaceElement;
524     gDidPassSurface = true;
525 
526     gPaintSession.CurrentlyDrawnItem = &tileElement;
527     gPaintSession.SurfaceElement = &surfaceElement;
528     gPaintSession.DidPassSurface = true;
529 
530     TestPaint::ResetEnvironment();
531     TestPaint::ResetTunnels();
532 
533     // TODO: Test Chainlift
534     // TODO: Test Maze
535     // TODO: Allow skip
536 
537     std::string state = String::Format("[trackSequence:%d chainLift:%d]", trackSequence, 0);
538 
539     SupportCall tileGeneralSupportCalls[4];
540     for (int direction = 0; direction < 4; direction++)
541     {
542         TestPaint::ResetSupportHeights();
543 
544         CallOriginal(rideType, trackType, direction, trackSequence, height, &tileElement);
545 
546         tileGeneralSupportCalls[direction].height = -1;
547         tileGeneralSupportCalls[direction].slope = -1;
548         if (gSupport.height != 0)
549         {
550             tileGeneralSupportCalls[direction].height = gSupport.height;
551         }
552         if (gSupport.slope != 0xFF)
553         {
554             tileGeneralSupportCalls[direction].slope = gSupport.slope;
555         }
556     }
557 
558     SupportCall referenceCall = tileGeneralSupportCalls[0];
559     if (!GeneralSupportHeightCall::CallsMatch(tileGeneralSupportCalls))
560     {
561         bool success = GeneralSupportHeightCall::FindMostCommonSupportCall(tileGeneralSupportCalls, &referenceCall);
562         if (!success)
563         {
564             *error += String::Format("Original support calls didn't match. %s\n", state.c_str());
565             for (int i = 0; i < 4; ++i)
566             {
567                 *error += String::Format("[%d, 0x%02X] ", tileGeneralSupportCalls[i].height, tileGeneralSupportCalls[i].slope);
568             }
569             *error += "\n";
570             return TEST_FAILED;
571         }
572     }
573 
574     for (int direction = 0; direction < 4; direction++)
575     {
576         TestPaint::ResetSupportHeights();
577 
578         TestPaint::testClearIgnore();
579         CallNew(rideType, trackType, direction, trackSequence, height, &tileElement);
580         if (TestPaint::testIsIgnored(direction, trackSequence))
581         {
582             continue;
583         }
584 
585         if (referenceCall.height != -1)
586         {
587             if (gPaintSession.Support.height != referenceCall.height)
588             {
589                 *error += String::Format(
590                     "General support heights didn't match. (expected height + %d, actual: height + %d) [direction:%d] %s\n",
591                     referenceCall.height - height, gPaintSession.Support.height - height, direction, state.c_str());
592                 return TEST_FAILED;
593             }
594         }
595         if (referenceCall.slope != -1)
596         {
597             if (gPaintSession.Support.slope != referenceCall.slope)
598             {
599                 *error += String::Format(
600                     "General support slopes didn't match. (expected 0x%02X, actual: 0x%02X) [direction:%d] %s\n",
601                     referenceCall.slope, gPaintSession.Support.slope, direction, state.c_str());
602                 return TEST_FAILED;
603             }
604         }
605     }
606 
607     return TEST_SUCCESS;
608 }
609 
TestTrackElementSideTunnels(uint8_t rideType,uint8_t trackType,uint8_t trackSequence,std::string * error)610 static uint8_t TestTrackElementSideTunnels(uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error)
611 {
612     uint16_t height = 3 * 16;
613 
614     TileElement tileElement = {};
615     tileElement.SetType(TILE_ELEMENT_TYPE_TRACK);
616     tileElement.SetLastForTile(true);
617     tileElement.AsTrack()->SetTrackType(trackType);
618     tileElement.base_height = height / 16;
619     g_currently_drawn_item = &tileElement;
620 
621     TileElement surfaceElement = {};
622     surfaceElement.SetType(TILE_ELEMENT_TYPE_SURFACE);
623     surfaceElement.base_height = MINIMUM_LAND_HEIGHT;
624     gSurfaceElement = &surfaceElement;
625     gDidPassSurface = true;
626 
627     gPaintSession.CurrentlyDrawnItem = &tileElement;
628     gPaintSession.SurfaceElement = &surfaceElement;
629     gPaintSession.DidPassSurface = true;
630 
631     TestPaint::ResetEnvironment();
632     TestPaint::ResetTunnels();
633 
634     TunnelCall tileTunnelCalls[4][4];
635 
636     // TODO: test inverted tracks
637 
638     for (int direction = 0; direction < 4; direction++)
639     {
640         TestPaint::ResetTunnels();
641 
642         for (int8_t offset = -8; offset <= 8; offset += 8)
643         {
644             CallOriginal(rideType, trackType, direction, trackSequence, height + offset, &tileElement);
645         }
646 
647         uint8_t rightIndex = (direction + 1) % 4;
648         uint8_t leftIndex = direction;
649 
650         for (int i = 0; i < 4; ++i)
651         {
652             tileTunnelCalls[direction][i].call = TUNNELCALL_SKIPPED;
653         }
654 
655         bool err = false;
656         tileTunnelCalls[direction][rightIndex] = SideTunnelCall::ExtractTunnelCalls(
657             gRightTunnels, gRightTunnelCount, height, &err);
658 
659         tileTunnelCalls[direction][leftIndex] = SideTunnelCall::ExtractTunnelCalls(
660             gLeftTunnels, gLeftTunnelCount, height, &err);
661 
662         if (err)
663         {
664             *error += "Multiple tunnels on one side aren't supported.\n";
665             return TEST_FAILED;
666         }
667     }
668 
669     TunnelCall newTileTunnelCalls[4][4];
670     for (int direction = 0; direction < 4; direction++)
671     {
672         TestPaint::ResetTunnels();
673 
674         TestPaint::testClearIgnore();
675 
676         for (int8_t offset = -8; offset <= 8; offset += 8)
677         {
678             // TODO: move tunnel pushing to interface so we don't have to check the output 3 times
679             CallNew(rideType, trackType, direction, trackSequence, height + offset, &tileElement);
680         }
681 
682         uint8_t rightIndex = (direction + 1) % 4;
683         uint8_t leftIndex = direction;
684 
685         for (int i = 0; i < 4; ++i)
686         {
687             newTileTunnelCalls[direction][i].call = TUNNELCALL_SKIPPED;
688         }
689 
690         bool err = false;
691         newTileTunnelCalls[direction][rightIndex] = SideTunnelCall::ExtractTunnelCalls(
692             gPaintSession.RightTunnels, gPaintSession.RightTunnelCount, height, &err);
693         newTileTunnelCalls[direction][leftIndex] = SideTunnelCall::ExtractTunnelCalls(
694             gPaintSession.LeftTunnels, gPaintSession.LeftTunnelCount, height, &err);
695         if (err)
696         {
697             *error += "Multiple tunnels on one side aren't supported.\n";
698             return TEST_FAILED;
699         }
700     }
701 
702     if (!SideTunnelCall::TunnelCallsLineUp(tileTunnelCalls))
703     {
704         // TODO: Check that new pattern uses the same tunnel group (round, big round, etc.)
705         *error += String::Format(
706             "Original tunnel calls don\'t line up. Skipping tunnel validation [trackSequence:%d].\n", trackSequence);
707         *error += Printer::PrintSideTunnelCalls(tileTunnelCalls);
708 
709         if (!SideTunnelCall::TunnelCallsLineUp(newTileTunnelCalls))
710         {
711             *error += String::Format("Decompiled tunnel calls don\'t line up. [trackSequence:%d].\n", trackSequence);
712             *error += Printer::PrintSideTunnelCalls(newTileTunnelCalls);
713             return TEST_FAILED;
714         }
715 
716         return TEST_SUCCESS;
717     }
718 
719     TunnelCall referencePattern[4];
720     SideTunnelCall::GetTunnelCallReferencePattern(tileTunnelCalls, &referencePattern);
721 
722     TunnelCall actualPattern[4];
723     SideTunnelCall::GetTunnelCallReferencePattern(newTileTunnelCalls, &actualPattern);
724 
725     if (!SideTunnelCall::TunnelPatternsMatch(referencePattern, actualPattern))
726     {
727         *error += String::Format("Tunnel calls don't match expected pattern. [trackSequence:%d]\n", trackSequence);
728         *error += " Expected:\n";
729         *error += Printer::PrintSideTunnelCalls(tileTunnelCalls);
730         *error += "   Actual:\n";
731         *error += Printer::PrintSideTunnelCalls(newTileTunnelCalls);
732         return TEST_FAILED;
733     }
734 
735     return TEST_SUCCESS;
736 }
737 
TestTrackElementVerticalTunnels(uint8_t rideType,uint8_t trackType,uint8_t trackSequence,std::string * error)738 static uint8_t TestTrackElementVerticalTunnels(uint8_t rideType, uint8_t trackType, uint8_t trackSequence, std::string* error)
739 {
740     uint16_t height = 3 * 16;
741 
742     TileElement tileElement = {};
743     tileElement.SetType(TILE_ELEMENT_TYPE_TRACK);
744     tileElement.SetLastForTile(true);
745     tileElement.AsTrack()->SetTrackType(trackType);
746     tileElement.base_height = height / 16;
747     g_currently_drawn_item = &tileElement;
748 
749     TileElement surfaceElement = {};
750     surfaceElement.SetType(TILE_ELEMENT_TYPE_SURFACE);
751     surfaceElement.base_height = MINIMUM_LAND_HEIGHT;
752     gSurfaceElement = &surfaceElement;
753     gDidPassSurface = true;
754 
755     gPaintSession.CurrentlyDrawnItem = &tileElement;
756     gPaintSession.SurfaceElement = &surfaceElement;
757     gPaintSession.DidPassSurface = true;
758 
759     TestPaint::ResetEnvironment();
760     TestPaint::ResetTunnels();
761 
762     uint16_t verticalTunnelHeights[4];
763 
764     for (int direction = 0; direction < 4; direction++)
765     {
766         uint8_t tunnelHeights[3] = { 0 };
767 
768         for (uint8_t i = 0; i < 3; i++)
769         {
770             gVerticalTunnelHeight = 0;
771             CallOriginal(rideType, trackType, direction, trackSequence, height - 8 + i * 8, &tileElement);
772             tunnelHeights[i] = gVerticalTunnelHeight;
773         }
774 
775         verticalTunnelHeights[direction] = VerticalTunnelCall::GetTunnelHeight(height, tunnelHeights);
776     }
777 
778     if (!VerticalTunnelCall::HeightIsConsistent(verticalTunnelHeights))
779     {
780         *error += String::Format(
781             "Original vertical tunnel height is inconsistent, skipping test. [trackSequence:%d]\n", trackSequence);
782         return TEST_SUCCESS;
783     }
784 
785     uint16_t referenceHeight = verticalTunnelHeights[0];
786 
787     for (int direction = 0; direction < 4; direction++)
788     {
789         TestPaint::testClearIgnore();
790 
791         testPaintVerticalTunnelHeight = 0;
792         CallNew(rideType, trackType, direction, trackSequence, height, &tileElement);
793 
794         if (TestPaint::testIsIgnored(direction, trackSequence))
795         {
796             continue;
797         }
798 
799         if (testPaintVerticalTunnelHeight != referenceHeight)
800         {
801             if (referenceHeight == 0)
802             {
803                 *error += String::Format(
804                     "Expected no tunnel. Actual: %s [trackSequence:%d]\n",
805                     Printer::PrintHeightOffset(testPaintVerticalTunnelHeight, height).c_str(), trackSequence);
806                 return TEST_FAILED;
807             }
808 
809             *error += String::Format(
810                 "Expected vertical tunnel height to be `%s`, was `%s`. [trackSequence:%d direction:%d]\n",
811                 Printer::PrintHeightOffset(referenceHeight, height).c_str(),
812                 Printer::PrintHeightOffset(testPaintVerticalTunnelHeight, height).c_str(), trackSequence, direction);
813 
814             return TEST_FAILED;
815         }
816     }
817 
818     return TEST_SUCCESS;
819 }
820