1 /*****************************************************************************
2  * Copyright (c) 2014-2020 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 "FootpathPlaceAction.h"
11 
12 #include "../Cheats.h"
13 #include "../OpenRCT2.h"
14 #include "../core/MemoryStream.h"
15 #include "../interface/Window.h"
16 #include "../localisation/StringIds.h"
17 #include "../management/Finance.h"
18 #include "../world/ConstructionClearance.h"
19 #include "../world/Footpath.h"
20 #include "../world/Location.hpp"
21 #include "../world/Park.h"
22 #include "../world/Scenery.h"
23 #include "../world/Surface.h"
24 #include "../world/TileElementsView.h"
25 #include "../world/Wall.h"
26 
27 using namespace OpenRCT2;
28 
FootpathPlaceAction(const CoordsXYZ & loc,uint8_t slope,ObjectEntryIndex type,ObjectEntryIndex railingsType,Direction direction,PathConstructFlags constructFlags)29 FootpathPlaceAction::FootpathPlaceAction(
30     const CoordsXYZ& loc, uint8_t slope, ObjectEntryIndex type, ObjectEntryIndex railingsType, Direction direction,
31     PathConstructFlags constructFlags)
32     : _loc(loc)
33     , _slope(slope)
34     , _type(type)
35     , _railingsType(railingsType)
36     , _direction(direction)
37     , _constructFlags(constructFlags)
38 {
39 }
40 
AcceptParameters(GameActionParameterVisitor & visitor)41 void FootpathPlaceAction::AcceptParameters(GameActionParameterVisitor& visitor)
42 {
43     visitor.Visit(_loc);
44     visitor.Visit("object", _type);
45     visitor.Visit("railingsObject", _railingsType);
46     visitor.Visit("direction", _direction);
47     visitor.Visit("slope", _slope);
48     visitor.Visit("constructFlags", _constructFlags);
49 }
50 
GetActionFlags() const51 uint16_t FootpathPlaceAction::GetActionFlags() const
52 {
53     return GameAction::GetActionFlags();
54 }
55 
Serialise(DataSerialiser & stream)56 void FootpathPlaceAction::Serialise(DataSerialiser& stream)
57 {
58     GameAction::Serialise(stream);
59 
60     stream << DS_TAG(_loc) << DS_TAG(_slope) << DS_TAG(_type) << DS_TAG(_railingsType) << DS_TAG(_direction)
61            << DS_TAG(_constructFlags);
62 }
63 
Query() const64 GameActions::Result::Ptr FootpathPlaceAction::Query() const
65 {
66     GameActions::Result::Ptr res = std::make_unique<GameActions::Result>();
67     res->Cost = 0;
68     res->Expenditure = ExpenditureType::Landscaping;
69     res->Position = _loc.ToTileCentre();
70 
71     gFootpathGroundFlags = 0;
72 
73     if (!LocationValid(_loc) || map_is_edge(_loc))
74     {
75         return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_BUILD_FOOTPATH_HERE, STR_OFF_EDGE_OF_MAP);
76     }
77 
78     if (!((gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) || gCheatsSandboxMode) && !map_is_location_owned(_loc))
79     {
80         return MakeResult(GameActions::Status::Disallowed, STR_CANT_BUILD_FOOTPATH_HERE, STR_LAND_NOT_OWNED_BY_PARK);
81     }
82 
83     if (_slope & SLOPE_IS_IRREGULAR_FLAG)
84     {
85         return MakeResult(GameActions::Status::Disallowed, STR_CANT_BUILD_FOOTPATH_HERE, STR_LAND_SLOPE_UNSUITABLE);
86     }
87 
88     if (_loc.z < FootpathMinHeight)
89     {
90         return MakeResult(GameActions::Status::Disallowed, STR_CANT_BUILD_FOOTPATH_HERE, STR_TOO_LOW);
91     }
92 
93     if (_loc.z > FootpathMaxHeight)
94     {
95         return MakeResult(GameActions::Status::Disallowed, STR_CANT_BUILD_FOOTPATH_HERE, STR_TOO_HIGH);
96     }
97 
98     if (_direction != INVALID_DIRECTION && !direction_valid(_direction))
99     {
100         log_error("Direction invalid. direction = %u", _direction);
101         return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_BUILD_FOOTPATH_HERE, STR_NONE);
102     }
103 
104     footpath_provisional_remove();
105     auto tileElement = map_get_footpath_element_slope(_loc, _slope);
106     if (tileElement == nullptr)
107     {
108         return ElementInsertQuery(std::move(res));
109     }
110     return ElementUpdateQuery(tileElement, std::move(res));
111 }
112 
Execute() const113 GameActions::Result::Ptr FootpathPlaceAction::Execute() const
114 {
115     GameActions::Result::Ptr res = std::make_unique<GameActions::Result>();
116     res->Cost = 0;
117     res->Expenditure = ExpenditureType::Landscaping;
118     res->Position = _loc.ToTileCentre();
119 
120     if (!(GetFlags() & GAME_COMMAND_FLAG_GHOST))
121     {
122         footpath_interrupt_peeps(_loc);
123     }
124 
125     gFootpathGroundFlags = 0;
126 
127     // Force ride construction to recheck area
128     _currentTrackSelectionFlags |= TRACK_SELECTION_FLAG_RECHECK;
129 
130     if (!(GetFlags() & GAME_COMMAND_FLAG_GHOST))
131     {
132         if (_direction != INVALID_DIRECTION && !gCheatsDisableClearanceChecks)
133         {
134             // It is possible, let's remove walls between the old and new piece of path
135             auto zLow = _loc.z;
136             auto zHigh = zLow + PATH_CLEARANCE;
137             wall_remove_intersecting_walls(
138                 { _loc, zLow, zHigh + ((_slope & TILE_ELEMENT_SURFACE_RAISED_CORNERS_MASK) ? 16 : 0) },
139                 direction_reverse(_direction));
140             wall_remove_intersecting_walls(
141                 { _loc.x - CoordsDirectionDelta[_direction].x, _loc.y - CoordsDirectionDelta[_direction].y, zLow, zHigh },
142                 _direction);
143         }
144     }
145 
146     auto tileElement = map_get_footpath_element_slope(_loc, _slope);
147     if (tileElement == nullptr)
148     {
149         return ElementInsertExecute(std::move(res));
150     }
151     return ElementUpdateExecute(tileElement, std::move(res));
152 }
153 
IsSameAsPathElement(const PathElement * pathElement) const154 bool FootpathPlaceAction::IsSameAsPathElement(const PathElement* pathElement) const
155 {
156     // Check if both this action and the element is queue
157     if (pathElement->IsQueue() != ((_constructFlags & PathConstructFlag::IsQueue) != 0))
158         return false;
159 
160     auto footpathObj = pathElement->GetLegacyPathEntry();
161     if (footpathObj == nullptr)
162     {
163         if (_constructFlags & PathConstructFlag::IsLegacyPathObject)
164         {
165             return false;
166         }
167 
168         return pathElement->GetSurfaceEntryIndex() == _type && pathElement->GetRailingsEntryIndex() == _railingsType;
169     }
170 
171     if (_constructFlags & PathConstructFlag::IsLegacyPathObject)
172     {
173         return pathElement->GetLegacyPathEntryIndex() == _type;
174     }
175 
176     return false;
177 }
178 
IsSameAsEntranceElement(const EntranceElement & entranceElement) const179 bool FootpathPlaceAction::IsSameAsEntranceElement(const EntranceElement& entranceElement) const
180 {
181     if (entranceElement.HasLegacyPathEntry())
182     {
183         if (_constructFlags & PathConstructFlag::IsLegacyPathObject)
184         {
185             return entranceElement.GetLegacyPathEntryIndex() == _type;
186         }
187 
188         return false;
189     }
190 
191     if (_constructFlags & PathConstructFlag::IsLegacyPathObject)
192     {
193         return false;
194     }
195 
196     return entranceElement.GetSurfaceEntryIndex() == _type;
197 }
198 
ElementUpdateQuery(PathElement * pathElement,GameActions::Result::Ptr res) const199 GameActions::Result::Ptr FootpathPlaceAction::ElementUpdateQuery(PathElement* pathElement, GameActions::Result::Ptr res) const
200 {
201     if (!IsSameAsPathElement(pathElement))
202     {
203         res->Cost += MONEY(6, 00);
204     }
205 
206     if (GetFlags() & GAME_COMMAND_FLAG_GHOST && !pathElement->IsGhost())
207     {
208         return MakeResult(GameActions::Status::Unknown, STR_CANT_BUILD_FOOTPATH_HERE, STR_NONE);
209     }
210     return res;
211 }
212 
ElementUpdateExecute(PathElement * pathElement,GameActions::Result::Ptr res) const213 GameActions::Result::Ptr FootpathPlaceAction::ElementUpdateExecute(PathElement* pathElement, GameActions::Result::Ptr res) const
214 {
215     if (!IsSameAsPathElement(pathElement))
216     {
217         res->Cost += MONEY(6, 00);
218     }
219 
220     footpath_queue_chain_reset();
221 
222     if (!(GetFlags() & GAME_COMMAND_FLAG_PATH_SCENERY))
223     {
224         footpath_remove_edges_at(_loc, reinterpret_cast<TileElement*>(pathElement));
225     }
226 
227     if (_constructFlags & PathConstructFlag::IsLegacyPathObject)
228     {
229         pathElement->SetLegacyPathEntryIndex(_type);
230     }
231     else
232     {
233         pathElement->SetSurfaceEntryIndex(_type);
234         pathElement->SetRailingsEntryIndex(_railingsType);
235     }
236 
237     pathElement->SetIsQueue((_constructFlags & PathConstructFlag::IsQueue) != 0);
238 
239     auto* elem = pathElement->GetAdditionEntry();
240     if (elem != nullptr)
241     {
242         if (_constructFlags & PathConstructFlag::IsQueue)
243         {
244             // remove any addition that isn't a TV or a lamp
245             if ((elem->flags & PATH_BIT_FLAG_IS_QUEUE_SCREEN) == 0 && (elem->flags & PATH_BIT_FLAG_LAMP) == 0)
246             {
247                 pathElement->SetIsBroken(false);
248                 pathElement->SetAddition(0);
249             }
250         }
251         else
252         {
253             // remove all TVs
254             if ((elem->flags & PATH_BIT_FLAG_IS_QUEUE_SCREEN) != 0)
255             {
256                 pathElement->SetIsBroken(false);
257                 pathElement->SetAddition(0);
258             }
259         }
260     }
261 
262     RemoveIntersectingWalls(pathElement);
263     return res;
264 }
265 
ElementInsertQuery(GameActions::Result::Ptr res) const266 GameActions::Result::Ptr FootpathPlaceAction::ElementInsertQuery(GameActions::Result::Ptr res) const
267 {
268     bool entrancePath = false, entranceIsSamePath = false;
269 
270     if (!MapCheckCapacityAndReorganise(_loc))
271     {
272         return MakeResult(GameActions::Status::NoFreeElements, STR_CANT_BUILD_FOOTPATH_HERE, STR_NONE);
273     }
274 
275     res->Cost = MONEY(12, 00);
276 
277     QuarterTile quarterTile{ 0b1111, 0 };
278     auto zLow = _loc.z;
279     auto zHigh = zLow + PATH_CLEARANCE;
280     if (_slope & FOOTPATH_PROPERTIES_FLAG_IS_SLOPED)
281     {
282         quarterTile = QuarterTile{ 0b1111, 0b1100 }.Rotate(_slope & TILE_ELEMENT_DIRECTION_MASK);
283         zHigh += PATH_HEIGHT_STEP;
284     }
285 
286     auto entranceElement = map_get_park_entrance_element_at(_loc, false);
287     // Make sure the entrance part is the middle
288     if (entranceElement != nullptr && (entranceElement->GetSequenceIndex()) == 0)
289     {
290         entrancePath = true;
291         // Make the price the same as replacing a path
292         if (IsSameAsEntranceElement(*entranceElement))
293             entranceIsSamePath = true;
294         else
295             res->Cost -= MONEY(6, 00);
296     }
297 
298     // Do not attempt to build a crossing with a queue or a sloped path.
299     auto isQueue = _constructFlags & PathConstructFlag::IsQueue;
300     uint8_t crossingMode = isQueue || (_slope != TILE_ELEMENT_SLOPE_FLAT) ? CREATE_CROSSING_MODE_NONE
301                                                                           : CREATE_CROSSING_MODE_PATH_OVER_TRACK;
302     auto canBuild = MapCanConstructWithClearAt(
303         { _loc, zLow, zHigh }, &map_place_non_scenery_clear_func, quarterTile, GetFlags(), crossingMode);
304     if (!entrancePath && canBuild->Error != GameActions::Status::Ok)
305     {
306         canBuild->ErrorTitle = STR_CANT_BUILD_FOOTPATH_HERE;
307         return canBuild;
308     }
309     res->Cost += canBuild->Cost;
310 
311     const auto clearanceData = canBuild->GetData<ConstructClearResult>();
312 
313     gFootpathGroundFlags = clearanceData.GroundFlags;
314     if (!gCheatsDisableClearanceChecks && (clearanceData.GroundFlags & ELEMENT_IS_UNDERWATER))
315     {
316         return MakeResult(GameActions::Status::Disallowed, STR_CANT_BUILD_FOOTPATH_HERE, STR_CANT_BUILD_THIS_UNDERWATER);
317     }
318 
319     auto surfaceElement = map_get_surface_element_at(_loc);
320     if (surfaceElement == nullptr)
321     {
322         return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_BUILD_FOOTPATH_HERE, STR_NONE);
323     }
324     int32_t supportHeight = zLow - surfaceElement->GetBaseZ();
325     res->Cost += supportHeight < 0 ? MONEY(20, 00) : (supportHeight / PATH_HEIGHT_STEP) * MONEY(5, 00);
326 
327     // Prevent the place sound from being spammed
328     if (entranceIsSamePath)
329         res->Cost = 0;
330 
331     return res;
332 }
333 
ElementInsertExecute(GameActions::Result::Ptr res) const334 GameActions::Result::Ptr FootpathPlaceAction::ElementInsertExecute(GameActions::Result::Ptr res) const
335 {
336     bool entrancePath = false, entranceIsSamePath = false;
337 
338     if (!(GetFlags() & (GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_GHOST)))
339     {
340         footpath_remove_litter(_loc);
341     }
342 
343     res->Cost = MONEY(12, 00);
344 
345     QuarterTile quarterTile{ 0b1111, 0 };
346     auto zLow = _loc.z;
347     auto zHigh = zLow + PATH_CLEARANCE;
348     if (_slope & FOOTPATH_PROPERTIES_FLAG_IS_SLOPED)
349     {
350         quarterTile = QuarterTile{ 0b1111, 0b1100 }.Rotate(_slope & TILE_ELEMENT_DIRECTION_MASK);
351         zHigh += PATH_HEIGHT_STEP;
352     }
353 
354     auto entranceElement = map_get_park_entrance_element_at(_loc, false);
355     // Make sure the entrance part is the middle
356     if (entranceElement != nullptr && (entranceElement->GetSequenceIndex()) == 0)
357     {
358         entrancePath = true;
359         // Make the price the same as replacing a path
360         if (IsSameAsEntranceElement(*entranceElement))
361             entranceIsSamePath = true;
362         else
363             res->Cost -= MONEY(6, 00);
364     }
365 
366     // Do not attempt to build a crossing with a queue or a sloped.
367     auto isQueue = _constructFlags & PathConstructFlag::IsQueue;
368     uint8_t crossingMode = isQueue || (_slope != TILE_ELEMENT_SLOPE_FLAT) ? CREATE_CROSSING_MODE_NONE
369                                                                           : CREATE_CROSSING_MODE_PATH_OVER_TRACK;
370     auto canBuild = MapCanConstructWithClearAt(
371         { _loc, zLow, zHigh }, &map_place_non_scenery_clear_func, quarterTile, GAME_COMMAND_FLAG_APPLY | GetFlags(),
372         crossingMode);
373     if (!entrancePath && canBuild->Error != GameActions::Status::Ok)
374     {
375         canBuild->ErrorTitle = STR_CANT_BUILD_FOOTPATH_HERE;
376         return canBuild;
377     }
378     res->Cost += canBuild->Cost;
379 
380     const auto clearanceData = canBuild->GetData<ConstructClearResult>();
381     gFootpathGroundFlags = clearanceData.GroundFlags;
382 
383     auto surfaceElement = map_get_surface_element_at(_loc);
384     if (surfaceElement == nullptr)
385     {
386         return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_BUILD_FOOTPATH_HERE, STR_NONE);
387     }
388     int32_t supportHeight = zLow - surfaceElement->GetBaseZ();
389     res->Cost += supportHeight < 0 ? MONEY(20, 00) : (supportHeight / PATH_HEIGHT_STEP) * MONEY(5, 00);
390 
391     if (entrancePath)
392     {
393         if (!(GetFlags() & GAME_COMMAND_FLAG_GHOST) && !entranceIsSamePath)
394         {
395             if (_constructFlags & PathConstructFlag::IsLegacyPathObject)
396             {
397                 entranceElement->SetLegacyPathEntryIndex(_type);
398             }
399             else
400             {
401                 entranceElement->SetSurfaceEntryIndex(_type);
402             }
403             map_invalidate_tile_full(_loc);
404         }
405     }
406     else
407     {
408         auto* pathElement = TileElementInsert<PathElement>(_loc, 0b1111);
409         Guard::Assert(pathElement != nullptr);
410 
411         pathElement->SetClearanceZ(zHigh);
412         if (_constructFlags & PathConstructFlag::IsLegacyPathObject)
413         {
414             pathElement->SetLegacyPathEntryIndex(_type);
415         }
416         else
417         {
418             pathElement->SetSurfaceEntryIndex(_type);
419             pathElement->SetRailingsEntryIndex(_railingsType);
420         }
421         pathElement->SetSlopeDirection(_slope & FOOTPATH_PROPERTIES_SLOPE_DIRECTION_MASK);
422         pathElement->SetSloped(_slope & FOOTPATH_PROPERTIES_FLAG_IS_SLOPED);
423         pathElement->SetIsQueue(isQueue);
424         pathElement->SetAddition(0);
425         pathElement->SetRideIndex(RIDE_ID_NULL);
426         pathElement->SetAdditionStatus(255);
427         pathElement->SetIsBroken(false);
428         pathElement->SetGhost(GetFlags() & GAME_COMMAND_FLAG_GHOST);
429 
430         footpath_queue_chain_reset();
431 
432         if (!(GetFlags() & GAME_COMMAND_FLAG_PATH_SCENERY))
433         {
434             footpath_remove_edges_at(_loc, pathElement->as<TileElement>());
435         }
436         if ((gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !(GetFlags() & GAME_COMMAND_FLAG_GHOST))
437         {
438             AutomaticallySetPeepSpawn();
439         }
440 
441         RemoveIntersectingWalls(pathElement);
442     }
443 
444     // Prevent the place sound from being spammed
445     if (entranceIsSamePath)
446         res->Cost = 0;
447 
448     return res;
449 }
450 
451 /**
452  *
453  *  rct2: 0x006A65AD
454  */
AutomaticallySetPeepSpawn() const455 void FootpathPlaceAction::AutomaticallySetPeepSpawn() const
456 {
457     uint8_t direction = 0;
458     if (_loc.x != 32)
459     {
460         direction++;
461         if (_loc.y != GetMapSizeUnits() - 32)
462         {
463             direction++;
464             if (_loc.x != GetMapSizeUnits() - 32)
465             {
466                 direction++;
467                 if (_loc.y != 32)
468                     return;
469             }
470         }
471     }
472 
473     if (gPeepSpawns.empty())
474     {
475         gPeepSpawns.emplace_back();
476     }
477     PeepSpawn* peepSpawn = &gPeepSpawns[0];
478     peepSpawn->x = _loc.x + (DirectionOffsets[direction].x * 15) + 16;
479     peepSpawn->y = _loc.y + (DirectionOffsets[direction].y * 15) + 16;
480     peepSpawn->direction = direction;
481     peepSpawn->z = _loc.z;
482 }
483 
RemoveIntersectingWalls(PathElement * pathElement) const484 void FootpathPlaceAction::RemoveIntersectingWalls(PathElement* pathElement) const
485 {
486     if (pathElement->IsSloped() && !(GetFlags() & GAME_COMMAND_FLAG_GHOST))
487     {
488         auto direction = pathElement->GetSlopeDirection();
489         int32_t z = pathElement->GetBaseZ();
490         wall_remove_intersecting_walls({ _loc, z, z + (6 * COORDS_Z_STEP) }, direction_reverse(direction));
491         wall_remove_intersecting_walls({ _loc, z, z + (6 * COORDS_Z_STEP) }, direction);
492         // Removing walls may have made the pointer invalid, so find it again
493         auto tileElement = map_get_footpath_element(CoordsXYZ(_loc, z));
494         if (tileElement == nullptr)
495         {
496             log_error("Something went wrong. Could not refind footpath.");
497             return;
498         }
499         pathElement = tileElement->AsPath();
500     }
501 
502     if (!(GetFlags() & GAME_COMMAND_FLAG_PATH_SCENERY))
503         footpath_connect_edges(_loc, reinterpret_cast<TileElement*>(pathElement), GetFlags());
504 
505     footpath_update_queue_chains();
506     map_invalidate_tile_full(_loc);
507 }
508 
map_get_footpath_element_slope(const CoordsXYZ & footpathPos,int32_t slope) const509 PathElement* FootpathPlaceAction::map_get_footpath_element_slope(const CoordsXYZ& footpathPos, int32_t slope) const
510 {
511     const bool isSloped = slope & FOOTPATH_PROPERTIES_FLAG_IS_SLOPED;
512     const auto slopeDirection = slope & FOOTPATH_PROPERTIES_SLOPE_DIRECTION_MASK;
513 
514     for (auto* pathElement : TileElementsView<PathElement>(footpathPos))
515     {
516         if (pathElement->GetBaseZ() != footpathPos.z)
517             continue;
518         if (pathElement->IsSloped() != isSloped)
519             continue;
520         if (pathElement->GetSlopeDirection() != slopeDirection)
521             continue;
522         return pathElement;
523     }
524 
525     return nullptr;
526 }
527