mirror of https://github.com/OpenRCT2/OpenRCT2.git
355 lines
12 KiB
C++
355 lines
12 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2024 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 "ConstructionClearance.h"
|
|
|
|
#include "../Game.h"
|
|
#include "../GameState.h"
|
|
#include "../localisation/Formatter.h"
|
|
#include "../object/LargeSceneryEntry.h"
|
|
#include "../object/SmallSceneryEntry.h"
|
|
#include "../object/WallSceneryEntry.h"
|
|
#include "../openrct2/Cheats.h"
|
|
#include "../ride/Ride.h"
|
|
#include "../ride/RideData.h"
|
|
#include "Park.h"
|
|
#include "Scenery.h"
|
|
#include "Surface.h"
|
|
|
|
using namespace OpenRCT2;
|
|
|
|
static int32_t MapPlaceClearFunc(
|
|
TileElement** tile_element, const CoordsXY& coords, uint8_t flags, money64* price, bool is_scenery)
|
|
{
|
|
if ((*tile_element)->GetType() != TileElementType::SmallScenery)
|
|
return 1;
|
|
|
|
if (is_scenery && !(flags & GAME_COMMAND_FLAG_TRACK_DESIGN))
|
|
return 1;
|
|
|
|
auto* scenery = (*tile_element)->AsSmallScenery()->GetEntry();
|
|
|
|
auto& gameState = GetGameState();
|
|
if (gameState.Park.Flags & PARK_FLAGS_FORBID_TREE_REMOVAL)
|
|
{
|
|
if (scenery != nullptr && scenery->HasFlag(SMALL_SCENERY_FLAG_IS_TREE))
|
|
return 1;
|
|
}
|
|
|
|
if (!(gameState.Park.Flags & PARK_FLAGS_NO_MONEY) && scenery != nullptr)
|
|
*price += scenery->removal_price;
|
|
|
|
if (flags & GAME_COMMAND_FLAG_GHOST)
|
|
return 0;
|
|
|
|
if (!(flags & GAME_COMMAND_FLAG_APPLY))
|
|
return 0;
|
|
|
|
MapInvalidateTile({ coords, (*tile_element)->GetBaseZ(), (*tile_element)->GetClearanceZ() });
|
|
|
|
TileElementRemove(*tile_element);
|
|
|
|
(*tile_element)--;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006E0D6E, 0x006B8D88
|
|
*/
|
|
int32_t MapPlaceSceneryClearFunc(TileElement** tile_element, const CoordsXY& coords, uint8_t flags, money64* price)
|
|
{
|
|
return MapPlaceClearFunc(tile_element, coords, flags, price, /*is_scenery=*/true);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006C5A4F, 0x006CDE57, 0x006A6733, 0x0066637E
|
|
*/
|
|
int32_t MapPlaceNonSceneryClearFunc(TileElement** tile_element, const CoordsXY& coords, uint8_t flags, money64* price)
|
|
{
|
|
return MapPlaceClearFunc(tile_element, coords, flags, price, /*is_scenery=*/false);
|
|
}
|
|
|
|
static bool MapLoc68BABCShouldContinue(
|
|
TileElement** tileElementPtr, const CoordsXYRangedZ& pos, CLEAR_FUNC clearFunc, uint8_t flags, money64& price,
|
|
uint8_t crossingMode, bool canBuildCrossing)
|
|
{
|
|
if (clearFunc != nullptr)
|
|
{
|
|
if (!clearFunc(tileElementPtr, pos, flags, &price))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Crossing mode 1: building track over path
|
|
auto tileElement = *tileElementPtr;
|
|
if (crossingMode == 1 && canBuildCrossing && tileElement->GetType() == TileElementType::Path
|
|
&& tileElement->GetBaseZ() == pos.baseZ && !tileElement->AsPath()->IsQueue() && !tileElement->AsPath()->IsSloped())
|
|
{
|
|
return true;
|
|
}
|
|
// Crossing mode 2: building path over track
|
|
else if (
|
|
crossingMode == 2 && canBuildCrossing && tileElement->GetType() == TileElementType::Track
|
|
&& tileElement->GetBaseZ() == pos.baseZ && tileElement->AsTrack()->GetTrackType() == TrackElemType::Flat)
|
|
{
|
|
auto ride = GetRide(tileElement->AsTrack()->GetRideIndex());
|
|
if (ride != nullptr && ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_SUPPORTS_LEVEL_CROSSINGS))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0068B932
|
|
* ax = x
|
|
* cx = y
|
|
* dl = zLow
|
|
* dh = zHigh
|
|
* ebp = clearFunc
|
|
* bl = bl
|
|
*/
|
|
GameActions::Result MapCanConstructWithClearAt(
|
|
const CoordsXYRangedZ& pos, CLEAR_FUNC clearFunc, QuarterTile quarterTile, uint8_t flags, uint8_t crossingMode, bool isTree)
|
|
{
|
|
auto res = GameActions::Result();
|
|
|
|
uint8_t groundFlags = ELEMENT_IS_ABOVE_GROUND;
|
|
res.SetData(ConstructClearResult{ groundFlags });
|
|
|
|
bool canBuildCrossing = false;
|
|
if (MapIsEdge(pos))
|
|
{
|
|
res.Error = GameActions::Status::InvalidParameters;
|
|
res.ErrorMessage = STR_OFF_EDGE_OF_MAP;
|
|
return res;
|
|
}
|
|
|
|
if (GetGameState().Cheats.DisableClearanceChecks)
|
|
{
|
|
res.SetData(ConstructClearResult{ groundFlags });
|
|
return res;
|
|
}
|
|
|
|
TileElement* tileElement = MapGetFirstElementAt(pos);
|
|
if (tileElement == nullptr)
|
|
{
|
|
res.Error = GameActions::Status::Unknown;
|
|
res.ErrorMessage = STR_NONE;
|
|
return res;
|
|
}
|
|
|
|
do
|
|
{
|
|
if (tileElement->GetType() != TileElementType::Surface)
|
|
{
|
|
if (pos.baseZ < tileElement->GetClearanceZ() && pos.clearanceZ > tileElement->GetBaseZ()
|
|
&& !(tileElement->IsGhost()))
|
|
{
|
|
if (tileElement->GetOccupiedQuadrants() & (quarterTile.GetBaseQuarterOccupied()))
|
|
{
|
|
if (MapLoc68BABCShouldContinue(
|
|
&tileElement, pos, clearFunc, flags, res.Cost, crossingMode, canBuildCrossing))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
MapGetObstructionErrorText(tileElement, res);
|
|
res.Error = GameActions::Status::NoClearance;
|
|
return res;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const auto waterHeight = tileElement->AsSurface()->GetWaterHeight();
|
|
if (waterHeight && waterHeight > pos.baseZ && tileElement->GetBaseZ() < pos.clearanceZ)
|
|
{
|
|
groundFlags |= ELEMENT_IS_UNDERWATER;
|
|
if (waterHeight < pos.clearanceZ)
|
|
{
|
|
if (clearFunc != nullptr && clearFunc(&tileElement, pos, flags, &res.Cost))
|
|
{
|
|
res.Error = GameActions::Status::NoClearance;
|
|
res.ErrorMessage = STR_CANNOT_BUILD_PARTLY_ABOVE_AND_PARTLY_BELOW_WATER;
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (GetGameState().Park.Flags & PARK_FLAGS_FORBID_HIGH_CONSTRUCTION && !isTree)
|
|
{
|
|
const auto heightFromGround = pos.clearanceZ - tileElement->GetBaseZ();
|
|
|
|
if (heightFromGround > (18 * COORDS_Z_STEP))
|
|
{
|
|
res.Error = GameActions::Status::Disallowed;
|
|
res.ErrorMessage = STR_LOCAL_AUTHORITY_WONT_ALLOW_CONSTRUCTION_ABOVE_TREE_HEIGHT;
|
|
return res;
|
|
}
|
|
}
|
|
|
|
// Only allow building crossings directly on a flat surface tile.
|
|
if (tileElement->GetType() == TileElementType::Surface
|
|
&& (tileElement->AsSurface()->GetSlope()) == TILE_ELEMENT_SLOPE_FLAT && tileElement->GetBaseZ() == pos.baseZ)
|
|
{
|
|
canBuildCrossing = true;
|
|
}
|
|
|
|
if (quarterTile.GetZQuarterOccupied() != 0b1111)
|
|
{
|
|
if (tileElement->GetBaseZ() >= pos.clearanceZ)
|
|
{
|
|
// Loc68BA81
|
|
groundFlags |= ELEMENT_IS_UNDERGROUND;
|
|
groundFlags &= ~ELEMENT_IS_ABOVE_GROUND;
|
|
}
|
|
else
|
|
{
|
|
auto northZ = tileElement->GetBaseZ();
|
|
auto eastZ = northZ;
|
|
auto southZ = northZ;
|
|
auto westZ = northZ;
|
|
const auto slope = tileElement->AsSurface()->GetSlope();
|
|
if (slope & TILE_ELEMENT_SLOPE_N_CORNER_UP)
|
|
{
|
|
northZ += LAND_HEIGHT_STEP;
|
|
if (slope == (TILE_ELEMENT_SLOPE_S_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT))
|
|
northZ += LAND_HEIGHT_STEP;
|
|
}
|
|
if (slope & TILE_ELEMENT_SLOPE_E_CORNER_UP)
|
|
{
|
|
eastZ += LAND_HEIGHT_STEP;
|
|
if (slope == (TILE_ELEMENT_SLOPE_W_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT))
|
|
eastZ += LAND_HEIGHT_STEP;
|
|
}
|
|
if (slope & TILE_ELEMENT_SLOPE_S_CORNER_UP)
|
|
{
|
|
southZ += LAND_HEIGHT_STEP;
|
|
if (slope == (TILE_ELEMENT_SLOPE_N_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT))
|
|
southZ += LAND_HEIGHT_STEP;
|
|
}
|
|
if (slope & TILE_ELEMENT_SLOPE_W_CORNER_UP)
|
|
{
|
|
westZ += LAND_HEIGHT_STEP;
|
|
if (slope == (TILE_ELEMENT_SLOPE_E_CORNER_DN | TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT))
|
|
westZ += LAND_HEIGHT_STEP;
|
|
}
|
|
const auto baseHeight = pos.baseZ + (4 * COORDS_Z_STEP);
|
|
const auto baseQuarter = quarterTile.GetBaseQuarterOccupied();
|
|
const auto zQuarter = quarterTile.GetZQuarterOccupied();
|
|
if ((!(baseQuarter & 0b0001) || ((zQuarter & 0b0001 || pos.baseZ >= northZ) && baseHeight >= northZ))
|
|
&& (!(baseQuarter & 0b0010) || ((zQuarter & 0b0010 || pos.baseZ >= eastZ) && baseHeight >= eastZ))
|
|
&& (!(baseQuarter & 0b0100) || ((zQuarter & 0b0100 || pos.baseZ >= southZ) && baseHeight >= southZ))
|
|
&& (!(baseQuarter & 0b1000) || ((zQuarter & 0b1000 || pos.baseZ >= westZ) && baseHeight >= westZ)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (MapLoc68BABCShouldContinue(&tileElement, pos, clearFunc, flags, res.Cost, crossingMode, canBuildCrossing))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
MapGetObstructionErrorText(tileElement, res);
|
|
res.Error = GameActions::Status::NoClearance;
|
|
return res;
|
|
}
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
res.SetData(ConstructClearResult{ groundFlags });
|
|
|
|
return res;
|
|
}
|
|
|
|
GameActions::Result MapCanConstructAt(const CoordsXYRangedZ& pos, QuarterTile bl)
|
|
{
|
|
return MapCanConstructWithClearAt(pos, nullptr, bl, 0);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0068BB18
|
|
*/
|
|
void MapGetObstructionErrorText(TileElement* tileElement, GameActions::Result& res)
|
|
{
|
|
Ride* ride;
|
|
|
|
res.ErrorMessage = STR_OBJECT_IN_THE_WAY;
|
|
switch (tileElement->GetType())
|
|
{
|
|
case TileElementType::Surface:
|
|
res.ErrorMessage = STR_RAISE_OR_LOWER_LAND_FIRST;
|
|
break;
|
|
case TileElementType::Path:
|
|
res.ErrorMessage = STR_FOOTPATH_IN_THE_WAY;
|
|
break;
|
|
case TileElementType::Track:
|
|
ride = GetRide(tileElement->AsTrack()->GetRideIndex());
|
|
if (ride != nullptr)
|
|
{
|
|
res.ErrorMessage = STR_X_IN_THE_WAY;
|
|
|
|
Formatter ft(res.ErrorMessageArgs.data());
|
|
ride->FormatNameTo(ft);
|
|
}
|
|
break;
|
|
case TileElementType::SmallScenery:
|
|
{
|
|
auto* sceneryEntry = tileElement->AsSmallScenery()->GetEntry();
|
|
res.ErrorMessage = STR_X_IN_THE_WAY;
|
|
auto ft = Formatter(res.ErrorMessageArgs.data());
|
|
StringId stringId = sceneryEntry != nullptr ? sceneryEntry->name : static_cast<StringId>(STR_EMPTY);
|
|
ft.Add<StringId>(stringId);
|
|
break;
|
|
}
|
|
case TileElementType::Entrance:
|
|
switch (tileElement->AsEntrance()->GetEntranceType())
|
|
{
|
|
case ENTRANCE_TYPE_RIDE_ENTRANCE:
|
|
res.ErrorMessage = STR_RIDE_ENTRANCE_IN_THE_WAY;
|
|
break;
|
|
case ENTRANCE_TYPE_RIDE_EXIT:
|
|
res.ErrorMessage = STR_RIDE_EXIT_IN_THE_WAY;
|
|
break;
|
|
case ENTRANCE_TYPE_PARK_ENTRANCE:
|
|
res.ErrorMessage = STR_PARK_ENTRANCE_IN_THE_WAY;
|
|
break;
|
|
}
|
|
break;
|
|
case TileElementType::Wall:
|
|
{
|
|
auto* wallEntry = tileElement->AsWall()->GetEntry();
|
|
res.ErrorMessage = STR_X_IN_THE_WAY;
|
|
auto ft = Formatter(res.ErrorMessageArgs.data());
|
|
StringId stringId = wallEntry != nullptr ? wallEntry->name : static_cast<StringId>(STR_EMPTY);
|
|
ft.Add<StringId>(stringId);
|
|
break;
|
|
}
|
|
case TileElementType::LargeScenery:
|
|
{
|
|
auto* sceneryEntry = tileElement->AsLargeScenery()->GetEntry();
|
|
res.ErrorMessage = STR_X_IN_THE_WAY;
|
|
auto ft = Formatter(res.ErrorMessageArgs.data());
|
|
StringId stringId = sceneryEntry != nullptr ? sceneryEntry->name : static_cast<StringId>(STR_EMPTY);
|
|
ft.Add<StringId>(stringId);
|
|
break;
|
|
}
|
|
case TileElementType::Banner:
|
|
break;
|
|
}
|
|
}
|