OpenRCT2/src/openrct2/actions/WallPlaceAction.cpp

609 lines
20 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 "WallPlaceAction.h"
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../management/Finance.h"
#include "../object/LargeSceneryEntry.h"
#include "../object/ObjectEntryManager.h"
#include "../object/SmallSceneryEntry.h"
#include "../object/WallSceneryEntry.h"
#include "../ride/Track.h"
#include "../ride/TrackDesign.h"
#include "../world/Banner.h"
#include "../world/ConstructionClearance.h"
#include "../world/MapAnimation.h"
#include "../world/Surface.h"
#include "../world/Wall.h"
using namespace OpenRCT2;
using namespace OpenRCT2::TrackMetaData;
WallPlaceAction::WallPlaceAction(
ObjectEntryIndex wallType, const CoordsXYZ& loc, uint8_t edge, int32_t primaryColour, int32_t secondaryColour,
int32_t tertiaryColour)
: _wallType(wallType)
, _loc(loc)
, _edge(edge)
, _primaryColour(primaryColour)
, _secondaryColour(secondaryColour)
, _tertiaryColour(tertiaryColour)
{
}
void WallPlaceAction::AcceptParameters(GameActionParameterVisitor& visitor)
{
visitor.Visit(_loc);
visitor.Visit("object", _wallType);
visitor.Visit("edge", _edge);
visitor.Visit("primaryColour", _primaryColour);
visitor.Visit("secondaryColour", _secondaryColour);
visitor.Visit("tertiaryColour", _tertiaryColour);
}
uint16_t WallPlaceAction::GetActionFlags() const
{
return GameAction::GetActionFlags();
}
void WallPlaceAction::Serialise(DataSerialiser& stream)
{
GameAction::Serialise(stream);
stream << DS_TAG(_wallType) << DS_TAG(_loc) << DS_TAG(_edge) << DS_TAG(_primaryColour) << DS_TAG(_secondaryColour)
<< DS_TAG(_tertiaryColour);
}
GameActions::Result WallPlaceAction::Query() const
{
auto res = GameActions::Result();
res.ErrorTitle = STR_CANT_BUILD_THIS_HERE;
res.Position = _loc;
res.Expenditure = ExpenditureType::Landscaping;
res.Position.x += 16;
res.Position.y += 16;
if (_loc.z == 0)
{
res.Position.z = TileElementHeight(res.Position);
}
if (!LocationValid(_loc))
{
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_OFF_EDGE_OF_MAP);
}
auto& gameState = GetGameState();
auto mapSizeMax = GetMapSizeMaxXY();
if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !(GetFlags() & GAME_COMMAND_FLAG_TRACK_DESIGN)
&& !gameState.Cheats.SandboxMode)
{
if (_loc.z == 0)
{
if (!MapIsLocationInPark(_loc))
{
return GameActions::Result(GameActions::Status::NotOwned, STR_CANT_BUILD_THIS_HERE, STR_LAND_NOT_OWNED_BY_PARK);
}
}
else if (!MapIsLocationOwned(_loc))
{
return GameActions::Result(GameActions::Status::NotOwned, STR_CANT_BUILD_THIS_HERE, STR_LAND_NOT_OWNED_BY_PARK);
}
}
else if (!_trackDesignDrawingPreview && (_loc.x > mapSizeMax.x || _loc.y > mapSizeMax.y))
{
LOG_ERROR("Invalid x/y coordinates. x = %d y = %d", _loc.x, _loc.y);
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_NONE);
}
if (_edge > 3)
{
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_NONE);
}
uint8_t edgeSlope = 0;
auto targetHeight = _loc.z;
if (targetHeight == 0)
{
auto* surfaceElement = MapGetSurfaceElementAt(_loc);
if (surfaceElement == nullptr)
{
LOG_ERROR("Surface element not found at %d, %d.", _loc.x, _loc.y);
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_NONE);
}
targetHeight = surfaceElement->GetBaseZ();
uint8_t slope = surfaceElement->GetSlope();
edgeSlope = GetWallSlopeFromEdgeSlope(slope, _edge & 3);
if (edgeSlope & EDGE_SLOPE_ELEVATED)
{
targetHeight += 16;
edgeSlope &= ~EDGE_SLOPE_ELEVATED;
}
}
auto* surfaceElement = MapGetSurfaceElementAt(_loc);
if (surfaceElement == nullptr)
{
LOG_ERROR("Surface element not found at %d, %d.", _loc.x, _loc.y);
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_NONE);
}
if (surfaceElement->GetWaterHeight() > 0)
{
uint16_t waterHeight = surfaceElement->GetWaterHeight();
if (targetHeight < waterHeight && !gameState.Cheats.DisableClearanceChecks)
{
return GameActions::Result(
GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_HERE, STR_CANT_BUILD_THIS_UNDERWATER);
}
}
if (targetHeight < surfaceElement->GetBaseZ() && !gameState.Cheats.DisableClearanceChecks)
{
return GameActions::Result(
GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
}
if (!(edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS)))
{
uint8_t newEdge = (_edge + 2) & 3;
uint8_t newBaseHeight = surfaceElement->BaseHeight;
newBaseHeight += 2;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
if (targetHeight / 8 < newBaseHeight && !gameState.Cheats.DisableClearanceChecks)
{
return GameActions::Result(
GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
}
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
{
newEdge = (newEdge - 1) & 3;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
newEdge = (newEdge + 2) & 3;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
newBaseHeight += 2;
if (targetHeight / 8 < newBaseHeight && !gameState.Cheats.DisableClearanceChecks)
{
return GameActions::Result(
GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_HERE,
STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
}
newBaseHeight -= 2;
}
}
}
}
newEdge = (_edge + 3) & 3;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
if (targetHeight / 8 < newBaseHeight && !gameState.Cheats.DisableClearanceChecks)
{
return GameActions::Result(
GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
}
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
{
newEdge = (newEdge - 1) & 3;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
newEdge = (newEdge + 2) & 3;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
newBaseHeight += 2;
if (targetHeight / 8 < newBaseHeight && !gameState.Cheats.DisableClearanceChecks)
{
return GameActions::Result(
GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_HERE,
STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
}
}
}
}
}
}
auto* wallEntry = ObjectManager::GetObjectEntry<WallSceneryEntry>(_wallType);
if (wallEntry == nullptr)
{
LOG_ERROR("Wall Type not found %d", _wallType);
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
}
if (wallEntry->scrolling_mode != SCROLLING_MODE_NONE)
{
if (HasReachedBannerLimit())
{
LOG_ERROR("No free banners available");
return GameActions::Result(
GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_TOO_MANY_BANNERS_IN_GAME);
}
}
uint8_t clearanceHeight = targetHeight / 8;
if (edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS))
{
if (wallEntry->flags & WALL_SCENERY_CANT_BUILD_ON_SLOPE)
{
return GameActions::Result(
GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_HERE, STR_ERR_UNABLE_TO_BUILD_THIS_ON_SLOPE);
}
clearanceHeight += 2;
}
clearanceHeight += wallEntry->height;
bool wallAcrossTrack = false;
if (!(GetFlags() & GAME_COMMAND_FLAG_TRACK_DESIGN) && !gameState.Cheats.DisableClearanceChecks)
{
auto result = WallCheckObstruction(wallEntry, targetHeight / 8, clearanceHeight, &wallAcrossTrack);
if (result.Error != GameActions::Status::Ok)
{
return result;
}
}
if (!MapCheckCapacityAndReorganise(_loc))
{
return GameActions::Result(
GameActions::Status::NoFreeElements, STR_CANT_BUILD_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
}
res.Cost = wallEntry->price;
res.SetData(WallPlaceActionResult{});
return res;
}
GameActions::Result WallPlaceAction::Execute() const
{
auto res = GameActions::Result();
auto& gameState = GetGameState();
res.ErrorTitle = STR_CANT_BUILD_THIS_HERE;
res.Position = _loc;
res.Expenditure = ExpenditureType::Landscaping;
res.Position.x += 16;
res.Position.y += 16;
if (res.Position.z == 0)
{
res.Position.z = TileElementHeight(res.Position);
}
uint8_t edgeSlope = 0;
auto targetHeight = _loc.z;
if (targetHeight == 0)
{
auto* surfaceElement = MapGetSurfaceElementAt(_loc);
if (surfaceElement == nullptr)
{
LOG_ERROR("Surface element not found at %d, %d.", _loc.x, _loc.y);
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_NONE);
}
targetHeight = surfaceElement->GetBaseZ();
uint8_t slope = surfaceElement->GetSlope();
edgeSlope = GetWallSlopeFromEdgeSlope(slope, _edge & 3);
if (edgeSlope & EDGE_SLOPE_ELEVATED)
{
targetHeight += 16;
edgeSlope &= ~EDGE_SLOPE_ELEVATED;
}
}
auto targetLoc = CoordsXYZ(_loc, targetHeight);
auto* wallEntry = ObjectManager::GetObjectEntry<WallSceneryEntry>(_wallType);
if (wallEntry == nullptr)
{
LOG_ERROR("Wall Type not found %d", _wallType);
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
}
uint8_t clearanceHeight = targetHeight / COORDS_Z_STEP;
if (edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS))
{
clearanceHeight += 2;
}
clearanceHeight += wallEntry->height;
bool wallAcrossTrack = false;
if (!(GetFlags() & GAME_COMMAND_FLAG_TRACK_DESIGN) && !gameState.Cheats.DisableClearanceChecks)
{
auto result = WallCheckObstruction(wallEntry, targetHeight / COORDS_Z_STEP, clearanceHeight, &wallAcrossTrack);
if (result.Error != GameActions::Status::Ok)
{
return result;
}
}
Banner* banner = nullptr;
if (wallEntry->scrolling_mode != SCROLLING_MODE_NONE)
{
banner = CreateBanner();
if (banner == nullptr)
{
LOG_ERROR("No free banners available");
return GameActions::Result(
GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_TOO_MANY_BANNERS_IN_GAME);
}
banner->text = {};
banner->colour = COLOUR_WHITE;
banner->text_colour = COLOUR_WHITE;
banner->flags = BANNER_FLAG_IS_WALL;
banner->type = 0; // Banner must be deleted after this point in an early return
banner->position = TileCoordsXY(_loc);
RideId rideIndex = BannerGetClosestRideIndex(targetLoc);
if (!rideIndex.IsNull())
{
banner->ride_index = rideIndex;
banner->flags |= BANNER_FLAG_LINKED_TO_RIDE;
}
}
auto* wallElement = TileElementInsert<WallElement>(targetLoc, 0b0000);
if (wallElement == nullptr)
{
return GameActions::Result(
GameActions::Status::NoFreeElements, STR_CANT_POSITION_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
}
wallElement->ClearanceHeight = clearanceHeight;
wallElement->SetDirection(_edge);
wallElement->SetSlope(edgeSlope);
wallElement->SetPrimaryColour(_primaryColour);
wallElement->SetSecondaryColour(_secondaryColour);
wallElement->SetAcrossTrack(wallAcrossTrack);
wallElement->SetEntryIndex(_wallType);
wallElement->SetBannerIndex(banner != nullptr ? banner->id : BannerIndex::GetNull());
if (wallEntry->flags & WALL_SCENERY_HAS_TERTIARY_COLOUR)
{
wallElement->SetTertiaryColour(_tertiaryColour);
}
wallElement->SetGhost(GetFlags() & GAME_COMMAND_FLAG_GHOST);
MapAnimationCreate(MAP_ANIMATION_TYPE_WALL, targetLoc);
MapInvalidateTileZoom1({ _loc, wallElement->GetBaseZ(), wallElement->GetBaseZ() + 72 });
res.Cost = wallEntry->price;
const auto bannerId = banner != nullptr ? banner->id : BannerIndex::GetNull();
res.SetData(WallPlaceActionResult{ wallElement->GetBaseZ(), bannerId });
return res;
}
/**
*
* rct2: 0x006E5CBA
*/
bool WallPlaceAction::WallCheckObstructionWithTrack(
const WallSceneryEntry* wall, int32_t z0, TrackElement* trackElement, bool* wallAcrossTrack) const
{
track_type_t trackType = trackElement->GetTrackType();
using namespace OpenRCT2::TrackMetaData;
const auto& ted = GetTrackElementDescriptor(trackType);
int32_t sequence = trackElement->GetSequenceIndex();
int32_t direction = (_edge - trackElement->GetDirection()) & kTileElementDirectionMask;
auto ride = GetRide(trackElement->GetRideIndex());
if (ride == nullptr)
{
return false;
}
if (TrackIsAllowedWallEdges(ride->type, trackType, sequence, direction))
{
return true;
}
if (!(wall->flags & WALL_SCENERY_IS_DOOR))
{
return false;
}
if (!ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_ALLOW_DOORS_ON_TRACK))
{
return false;
}
*wallAcrossTrack = true;
if (z0 & 1)
{
return false;
}
int32_t z;
if (sequence == 0)
{
if (std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_DISALLOW_DOORS)
{
return false;
}
if (ted.Definition.RollStart == TrackRoll::None)
{
if (!(ted.Coordinates.rotation_begin & 4))
{
direction = DirectionReverse(trackElement->GetDirection());
if (direction == _edge)
{
const PreviewTrack* trackBlock = ted.GetBlockForSequence(sequence);
z = ted.Coordinates.z_begin;
z = trackElement->BaseHeight + ((z - trackBlock->z) * 8);
if (z == z0)
{
return true;
}
}
}
}
}
const PreviewTrack* trackBlock = &ted.Block[sequence + 1];
if (trackBlock->index != 0xFF)
{
return false;
}
if (ted.Definition.RollEnd != TrackRoll::None)
{
return false;
}
direction = ted.Coordinates.rotation_end;
if (direction & 4)
{
return false;
}
direction = (trackElement->GetDirection() + ted.Coordinates.rotation_end) & kTileElementDirectionMask;
if (direction != _edge)
{
return false;
}
trackBlock = ted.GetBlockForSequence(sequence);
z = ted.Coordinates.z_end;
z = trackElement->BaseHeight + ((z - trackBlock->z) * 8);
return z == z0;
}
/**
*
* rct2: 0x006E5C1A
*/
GameActions::Result WallPlaceAction::WallCheckObstruction(
const WallSceneryEntry* wall, int32_t z0, int32_t z1, bool* wallAcrossTrack) const
{
*wallAcrossTrack = false;
if (MapIsLocationAtEdge(_loc))
{
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_BUILD_THIS_HERE, STR_OFF_EDGE_OF_MAP);
}
TileElement* tileElement = MapGetFirstElementAt(_loc);
do
{
if (tileElement == nullptr)
break;
auto elementType = tileElement->GetType();
if (elementType == TileElementType::Surface)
continue;
if (tileElement->IsGhost())
continue;
if (z0 >= tileElement->ClearanceHeight)
continue;
if (z1 <= tileElement->BaseHeight)
continue;
if (elementType == TileElementType::Wall)
{
int32_t direction = tileElement->GetDirection();
if (_edge == direction)
{
auto res = GameActions::Result(GameActions::Status::NoClearance, STR_CANT_BUILD_THIS_HERE, STR_NONE);
MapGetObstructionErrorText(tileElement, res);
return res;
}
continue;
}
if (tileElement->GetOccupiedQuadrants() == 0)
continue;
auto res = GameActions::Result(GameActions::Status::NoClearance, STR_CANT_BUILD_THIS_HERE, STR_NONE);
switch (elementType)
{
case TileElementType::Entrance:
MapGetObstructionErrorText(tileElement, res);
return res;
case TileElementType::Path:
if (tileElement->AsPath()->GetEdges() & (1 << _edge))
{
MapGetObstructionErrorText(tileElement, res);
return res;
}
break;
case TileElementType::LargeScenery:
{
const auto* largeSceneryElement = tileElement->AsLargeScenery();
const auto* sceneryEntry = largeSceneryElement->GetEntry();
// If there is no entry, assume the object is not in the way.
if (sceneryEntry == nullptr)
break;
auto sequence = largeSceneryElement->GetSequenceIndex();
const LargeSceneryTile& tile = sceneryEntry->tiles[sequence];
int32_t direction = ((_edge - tileElement->GetDirection()) & kTileElementDirectionMask) + 8;
if (!(tile.flags & (1 << direction)))
{
MapGetObstructionErrorText(tileElement, res);
return res;
}
break;
}
case TileElementType::SmallScenery:
{
auto sceneryEntry = tileElement->AsSmallScenery()->GetEntry();
if (sceneryEntry != nullptr && sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_NO_WALLS))
{
MapGetObstructionErrorText(tileElement, res);
return res;
}
break;
}
case TileElementType::Track:
if (!WallCheckObstructionWithTrack(wall, z0, tileElement->AsTrack(), wallAcrossTrack))
{
return res;
}
break;
default:
break;
}
} while (!(tileElement++)->IsLastForTile());
return GameActions::Result();
}
bool WallPlaceAction::TrackIsAllowedWallEdges(
ride_type_t rideType, track_type_t trackType, uint8_t trackSequence, uint8_t direction)
{
if (!GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_TRACK_NO_WALLS))
{
const auto& ted = GetTrackElementDescriptor(trackType);
if (ted.SequenceElementAllowedWallEdges[trackSequence] & (1 << direction))
{
return true;
}
}
return false;
}