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