/***************************************************************************** * Copyright (c) 2014-2022 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "LandSetHeightAction.h" #include "../Context.h" #include "../OpenRCT2.h" #include "../interface/Window.h" #include "../localisation/Localisation.h" #include "../localisation/StringIds.h" #include "../management/Finance.h" #include "../ride/RideData.h" #include "../windows/Intent.h" #include "../world/ConstructionClearance.h" #include "../world/Park.h" #include "../world/Scenery.h" #include "../world/SmallScenery.h" #include "../world/Surface.h" #include "../world/TileElementsView.h" using namespace OpenRCT2; LandSetHeightAction::LandSetHeightAction(const CoordsXY& coords, uint8_t height, uint8_t style) : _coords(coords) , _height(height) , _style(style) { } uint16_t LandSetHeightAction::GetActionFlags() const { return GameAction::GetActionFlags() | GameActions::Flags::EditorOnly; } void LandSetHeightAction::Serialise(DataSerialiser& stream) { GameAction::Serialise(stream); stream << DS_TAG(_coords) << DS_TAG(_height) << DS_TAG(_style); } GameActions::Result LandSetHeightAction::Query() const { if (gParkFlags & PARK_FLAGS_FORBID_LANDSCAPE_CHANGES) { return GameActions::Result(GameActions::Status::Disallowed, STR_FORBIDDEN_BY_THE_LOCAL_AUTHORITY, STR_NONE); } rct_string_id errorTitle = CheckParameters(); if (errorTitle != STR_NONE) { return GameActions::Result(GameActions::Status::Disallowed, errorTitle, STR_NONE); } if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !gCheatsSandboxMode) { if (!map_is_location_in_park(_coords)) { return GameActions::Result(GameActions::Status::Disallowed, STR_LAND_NOT_OWNED_BY_PARK, STR_NONE); } } money32 sceneryRemovalCost = 0; if (!gCheatsDisableClearanceChecks) { if (gParkFlags & PARK_FLAGS_FORBID_TREE_REMOVAL) { // Check for obstructing large trees TileElement* tileElement = CheckTreeObstructions(); if (tileElement != nullptr) { auto res = GameActions::Result(GameActions::Status::Disallowed, STR_NONE, STR_NONE); map_obstruction_set_error_text(tileElement, res); return res; } } sceneryRemovalCost = GetSmallSceneryRemovalCost(); } // Check for ride support limits if (!gCheatsDisableSupportLimits) { errorTitle = CheckRideSupports(); if (errorTitle != STR_NONE) { return GameActions::Result(GameActions::Status::Disallowed, errorTitle, STR_NONE); } } auto* surfaceElement = map_get_surface_element_at(_coords); if (surfaceElement == nullptr) return GameActions::Result(GameActions::Status::Unknown, STR_NONE, STR_NONE); // We need to check if there is _currently_ a level crossing on the tile. // For that, we need the old height, so we can't use the _height variable. auto oldCoords = CoordsXYZ{ _coords, surfaceElement->GetBaseZ() }; auto* pathElement = map_get_footpath_element(oldCoords); if (pathElement != nullptr && pathElement->AsPath()->IsLevelCrossing(oldCoords)) { return GameActions::Result(GameActions::Status::Disallowed, STR_REMOVE_LEVEL_CROSSING_FIRST, STR_NONE); } TileElement* tileElement = CheckFloatingStructures(reinterpret_cast(surfaceElement), _height); if (tileElement != nullptr) { auto res = GameActions::Result(GameActions::Status::Disallowed, STR_NONE, STR_NONE); map_obstruction_set_error_text(tileElement, res); return res; } if (!gCheatsDisableClearanceChecks) { uint8_t zCorner = _height; if (_style & TILE_ELEMENT_SURFACE_RAISED_CORNERS_MASK) { zCorner += 2; if (_style & TILE_ELEMENT_SURFACE_DIAGONAL_FLAG) { zCorner += 2; } } auto clearResult = MapCanConstructWithClearAt( { _coords, _height * COORDS_Z_STEP, zCorner * COORDS_Z_STEP }, &map_set_land_height_clear_func, { 0b1111, 0 }, 0, CREATE_CROSSING_MODE_NONE); if (clearResult.Error != GameActions::Status::Ok) { clearResult.Error = GameActions::Status::Disallowed; return clearResult; } } auto res = GameActions::Result(); res.Cost = sceneryRemovalCost + GetSurfaceHeightChangeCost(surfaceElement); res.Expenditure = ExpenditureType::Landscaping; return res; } GameActions::Result LandSetHeightAction::Execute() const { money32 cost = 0.00_GBP; auto surfaceHeight = tile_element_height(_coords); footpath_remove_litter({ _coords, surfaceHeight }); if (!gCheatsDisableClearanceChecks) { wall_remove_at({ _coords, _height * 8 - 16, _height * 8 + 32 }); cost += GetSmallSceneryRemovalCost(); SmallSceneryRemoval(); } auto* surfaceElement = map_get_surface_element_at(_coords); if (surfaceElement == nullptr) return GameActions::Result(GameActions::Status::Unknown, STR_NONE, STR_NONE); cost += GetSurfaceHeightChangeCost(surfaceElement); SetSurfaceHeight(reinterpret_cast(surfaceElement)); auto res = GameActions::Result(); res.Position = { _coords.x + 16, _coords.y + 16, surfaceHeight }; res.Cost = cost; res.Expenditure = ExpenditureType::Landscaping; return res; } rct_string_id LandSetHeightAction::CheckParameters() const { if (!LocationValid(_coords)) { return STR_OFF_EDGE_OF_MAP; } auto mapSizeMax = GetMapSizeMaxXY(); if (_coords.x > mapSizeMax.x || _coords.y > mapSizeMax.y) { return STR_OFF_EDGE_OF_MAP; } if (_height < MINIMUM_LAND_HEIGHT) { return STR_TOO_LOW; } // Divide by 2 and subtract 7 to get the in-game units. if (_height > MAXIMUM_LAND_HEIGHT) { return STR_TOO_HIGH; } if (_height > MAXIMUM_LAND_HEIGHT - 2 && (_style & TILE_ELEMENT_SURFACE_SLOPE_MASK) != 0) { return STR_TOO_HIGH; } if (_height == MAXIMUM_LAND_HEIGHT - 2 && (_style & TILE_ELEMENT_SURFACE_DIAGONAL_FLAG)) { return STR_TOO_HIGH; } return STR_NONE; } TileElement* LandSetHeightAction::CheckTreeObstructions() const { for (auto* sceneryElement : TileElementsView(_coords)) { if (_height > sceneryElement->clearance_height) continue; if (_height + 4 < sceneryElement->base_height) continue; auto* sceneryEntry = sceneryElement->GetEntry(); if (!sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_IS_TREE)) continue; return sceneryElement->as(); } return nullptr; } money32 LandSetHeightAction::GetSmallSceneryRemovalCost() const { money32 cost{ 0 }; for (auto* sceneryElement : TileElementsView(_coords)) { if (_height > sceneryElement->clearance_height) continue; if (_height + 4 < sceneryElement->base_height) continue; auto* sceneryEntry = sceneryElement->GetEntry(); if (sceneryEntry == nullptr) continue; cost += sceneryEntry->removal_price; } return cost; } void LandSetHeightAction::SmallSceneryRemoval() const { TileElement* tileElement = map_get_first_element_at(_coords); do { if (tileElement == nullptr) break; if (tileElement->GetType() != TileElementType::SmallScenery) continue; if (_height > tileElement->clearance_height) continue; if (_height + 4 < tileElement->base_height) continue; tile_element_remove(tileElement--); } while (!(tileElement++)->IsLastForTile()); } rct_string_id LandSetHeightAction::CheckRideSupports() const { for (auto* trackElement : TileElementsView(_coords)) { RideId rideIndex = trackElement->GetRideIndex(); auto ride = get_ride(rideIndex); if (ride == nullptr) continue; rct_ride_entry* rideEntry = ride->GetRideEntry(); if (rideEntry == nullptr) continue; int32_t maxHeight = rideEntry->max_height; if (maxHeight == 0) { maxHeight = ride->GetRideTypeDescriptor().Heights.MaxHeight; } int32_t zDelta = trackElement->clearance_height - _height; if (zDelta >= 0 && zDelta / 2 > maxHeight) { return STR_SUPPORTS_CANT_BE_EXTENDED; } } return STR_NONE; } TileElement* LandSetHeightAction::CheckFloatingStructures(TileElement* surfaceElement, uint8_t zCorner) const { if (surfaceElement->AsSurface()->HasTrackThatNeedsWater()) { uint32_t waterHeight = surfaceElement->AsSurface()->GetWaterHeight(); if (waterHeight != 0) { if (_style & TILE_ELEMENT_SURFACE_SLOPE_MASK) { zCorner += 2; if (_style & TILE_ELEMENT_SURFACE_DIAGONAL_FLAG) { zCorner += 2; } } if (zCorner > (waterHeight / COORDS_Z_STEP) - 2) { return ++surfaceElement; } } } return nullptr; } money32 LandSetHeightAction::GetSurfaceHeightChangeCost(SurfaceElement* surfaceElement) const { money32 cost{ 0 }; for (Direction i : ALL_DIRECTIONS) { int32_t cornerHeight = tile_element_get_corner_height(surfaceElement, i); cornerHeight -= map_get_corner_height(_height, _style & TILE_ELEMENT_SURFACE_SLOPE_MASK, i); cost += 2.50_GBP * abs(cornerHeight); } return cost; } void LandSetHeightAction::SetSurfaceHeight(TileElement* surfaceElement) const { surfaceElement->base_height = _height; surfaceElement->clearance_height = _height; surfaceElement->AsSurface()->SetSlope(_style); int32_t waterHeight = surfaceElement->AsSurface()->GetWaterHeight() / COORDS_Z_STEP; if (waterHeight != 0 && waterHeight <= _height) { surfaceElement->AsSurface()->SetWaterHeight(0); } map_invalidate_tile_full(_coords); } int32_t LandSetHeightAction::map_set_land_height_clear_func( TileElement** tile_element, [[maybe_unused]] const CoordsXY& coords, [[maybe_unused]] uint8_t flags, [[maybe_unused]] money32* price) { if ((*tile_element)->GetType() == TileElementType::Surface) return 0; if ((*tile_element)->GetType() == TileElementType::SmallScenery) return 0; return 1; }