OpenRCT2/src/openrct2/actions/WallPlaceAction.hpp

678 lines
24 KiB
C++

/*****************************************************************************
* Copyright (c) 2014-2020 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.
*****************************************************************************/
#pragma once
#include "../OpenRCT2.h"
#include "../management/Finance.h"
#include "../ride/RideData.h"
#include "../ride/Track.h"
#include "../ride/TrackData.h"
#include "../ride/TrackDesign.h"
#include "../world/Banner.h"
#include "../world/LargeScenery.h"
#include "../world/MapAnimation.h"
#include "../world/Scenery.h"
#include "../world/SmallScenery.h"
#include "../world/Surface.h"
#include "../world/Wall.h"
#include "GameAction.h"
class WallPlaceActionResult final : public GameActions::Result
{
public:
WallPlaceActionResult()
: GameActions::Result(GameActions::Status::Ok, STR_CANT_BUILD_PARK_ENTRANCE_HERE)
{
}
WallPlaceActionResult(GameActions::Status err)
: GameActions::Result(err, STR_CANT_BUILD_PARK_ENTRANCE_HERE)
{
}
WallPlaceActionResult(GameActions::Status err, rct_string_id msg)
: GameActions::Result(err, STR_CANT_BUILD_PARK_ENTRANCE_HERE, msg)
{
}
WallPlaceActionResult(GameActions::Status error, rct_string_id msg, uint8_t* args)
: GameActions::Result(error, STR_CANT_BUILD_PARK_ENTRANCE_HERE, msg, args)
{
}
TileElement* tileElement = nullptr;
};
DEFINE_GAME_ACTION(WallPlaceAction, GAME_COMMAND_PLACE_WALL, WallPlaceActionResult)
{
private:
ObjectEntryIndex _wallType{ OBJECT_ENTRY_INDEX_NULL };
CoordsXYZ _loc;
Direction _edge{ INVALID_DIRECTION };
int32_t _primaryColour{ COLOUR_BLACK };
int32_t _secondaryColour{ COLOUR_BLACK };
int32_t _tertiaryColour{ COLOUR_BLACK };
BannerIndex _bannerId{ BANNER_INDEX_NULL };
public:
WallPlaceAction() = default;
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)
{
rct_scenery_entry* sceneryEntry = get_wall_entry(_wallType);
if (sceneryEntry != nullptr)
{
if (sceneryEntry->wall.scrolling_mode != SCROLLING_MODE_NONE)
{
_bannerId = create_new_banner(0);
}
}
}
void AcceptParameters(GameActionParameterVisitor & visitor) override
{
visitor.Visit(_loc);
visitor.Visit("object", _wallType);
visitor.Visit("edge", _edge);
visitor.Visit("primaryColour", _primaryColour);
visitor.Visit("secondaryColour", _secondaryColour);
visitor.Visit("tertiaryColour", _tertiaryColour);
rct_scenery_entry* sceneryEntry = get_large_scenery_entry(_wallType);
if (sceneryEntry != nullptr)
{
if (sceneryEntry->large_scenery.scrolling_mode != SCROLLING_MODE_NONE)
{
_bannerId = create_new_banner(0);
}
}
}
uint16_t GetActionFlags() const override
{
return GameAction::GetActionFlags();
}
void Serialise(DataSerialiser & stream) override
{
GameAction::Serialise(stream);
stream << DS_TAG(_wallType) << DS_TAG(_loc) << DS_TAG(_edge) << DS_TAG(_primaryColour) << DS_TAG(_secondaryColour)
<< DS_TAG(_tertiaryColour) << DS_TAG(_bannerId);
}
GameActions::Result::Ptr Query() const override
{
auto res = std::make_unique<WallPlaceActionResult>();
res->ErrorTitle = STR_CANT_BUILD_PARK_ENTRANCE_HERE;
res->Position = _loc;
res->Expenditure = ExpenditureType::Landscaping;
res->Position.x += 16;
res->Position.y += 16;
if (_loc.z == 0)
{
res->Position.z = tile_element_height(res->Position);
}
if (!LocationValid(_loc))
{
return MakeResult(GameActions::Status::NotOwned);
}
if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !(GetFlags() & GAME_COMMAND_FLAG_PATH_SCENERY)
&& !gCheatsSandboxMode)
{
if (_loc.z == 0)
{
if (!map_is_location_in_park(_loc))
{
return std::make_unique<WallPlaceActionResult>(GameActions::Status::NotOwned);
}
}
else if (!map_is_location_owned(_loc))
{
return std::make_unique<WallPlaceActionResult>(GameActions::Status::NotOwned);
}
}
else if (!byte_9D8150 && (_loc.x > gMapSizeMaxXY || _loc.y > gMapSizeMaxXY))
{
log_error("Invalid x/y coordinates. x = %d y = %d", _loc.x, _loc.y);
return std::make_unique<WallPlaceActionResult>(GameActions::Status::InvalidParameters);
}
if (_edge > 3)
{
return std::make_unique<WallPlaceActionResult>(GameActions::Status::InvalidParameters);
}
uint8_t edgeSlope = 0;
auto targetHeight = _loc.z;
if (targetHeight == 0)
{
auto* surfaceElement = map_get_surface_element_at(_loc);
if (surfaceElement == nullptr)
{
log_error("Surface element not found at %d, %d.", _loc.x, _loc.y);
return std::make_unique<WallPlaceActionResult>(GameActions::Status::InvalidParameters);
}
targetHeight = surfaceElement->GetBaseZ();
uint8_t slope = surfaceElement->GetSlope();
edgeSlope = LandSlopeToWallSlope[slope][_edge & 3];
if (edgeSlope & EDGE_SLOPE_ELEVATED)
{
targetHeight += 16;
edgeSlope &= ~EDGE_SLOPE_ELEVATED;
}
}
auto* surfaceElement = map_get_surface_element_at(_loc);
if (surfaceElement == nullptr)
{
log_error("Surface element not found at %d, %d.", _loc.x, _loc.y);
return std::make_unique<WallPlaceActionResult>(GameActions::Status::InvalidParameters);
}
if (surfaceElement->GetWaterHeight() > 0)
{
uint16_t waterHeight = surfaceElement->GetWaterHeight();
if (targetHeight < waterHeight && !gCheatsDisableClearanceChecks)
{
return std::make_unique<WallPlaceActionResult>(GameActions::Status::Disallowed, STR_CANT_BUILD_THIS_UNDERWATER);
}
}
if (targetHeight < surfaceElement->GetBaseZ() && !gCheatsDisableClearanceChecks)
{
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::Disallowed, 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->base_height;
newBaseHeight += 2;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
if (targetHeight / 8 < newBaseHeight)
{
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::Disallowed, 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)
{
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::Disallowed, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
}
newBaseHeight -= 2;
}
}
}
}
newEdge = (_edge + 3) & 3;
if (surfaceElement->GetSlope() & (1 << newEdge))
{
if (targetHeight / 8 < newBaseHeight)
{
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::Disallowed, 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)
{
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::Disallowed, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
}
}
}
}
}
}
rct_scenery_entry* wallEntry = get_wall_entry(_wallType);
if (wallEntry == nullptr)
{
log_error("Wall Type not found %d", _wallType);
return std::make_unique<WallPlaceActionResult>(GameActions::Status::InvalidParameters);
}
if (wallEntry->wall.scrolling_mode != SCROLLING_MODE_NONE)
{
if (_bannerId == BANNER_INDEX_NULL)
{
log_error("Banner Index not specified.");
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::InvalidParameters, STR_TOO_MANY_BANNERS_IN_GAME);
}
auto banner = GetBanner(_bannerId);
if (!banner->IsNull())
{
log_error("No free banners available");
return std::make_unique<WallPlaceActionResult>(GameActions::Status::NoFreeElements);
}
}
uint8_t clearanceHeight = targetHeight / 8;
if (edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS))
{
if (wallEntry->wall.flags & WALL_SCENERY_CANT_BUILD_ON_SLOPE)
{
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::Disallowed, STR_ERR_UNABLE_TO_BUILD_THIS_ON_SLOPE);
}
clearanceHeight += 2;
}
clearanceHeight += wallEntry->wall.height;
bool wallAcrossTrack = false;
if (!(GetFlags() & GAME_COMMAND_FLAG_PATH_SCENERY) && !gCheatsDisableClearanceChecks)
{
auto result = WallCheckObstruction(wallEntry, targetHeight / 8, clearanceHeight, &wallAcrossTrack);
if (result->Error != GameActions::Status::Ok)
{
return result;
}
}
if (!map_check_free_elements_and_reorganise(1))
{
return MakeResult(GameActions::Status::NoFreeElements, STR_TILE_ELEMENT_LIMIT_REACHED);
}
res->Cost = wallEntry->wall.price;
return res;
}
GameActions::Result::Ptr Execute() const override
{
auto res = std::make_unique<WallPlaceActionResult>();
res->ErrorTitle = STR_CANT_BUILD_PARK_ENTRANCE_HERE;
res->Position = _loc;
res->Expenditure = ExpenditureType::Landscaping;
res->Position.x += 16;
res->Position.y += 16;
if (res->Position.z == 0)
{
res->Position.z = tile_element_height(res->Position);
}
uint8_t edgeSlope = 0;
auto targetHeight = _loc.z;
if (targetHeight == 0)
{
auto* surfaceElement = map_get_surface_element_at(_loc);
if (surfaceElement == nullptr)
{
log_error("Surface element not found at %d, %d.", _loc.x, _loc.y);
return std::make_unique<WallPlaceActionResult>(GameActions::Status::InvalidParameters);
}
targetHeight = surfaceElement->GetBaseZ();
uint8_t slope = surfaceElement->GetSlope();
edgeSlope = LandSlopeToWallSlope[slope][_edge & 3];
if (edgeSlope & EDGE_SLOPE_ELEVATED)
{
targetHeight += 16;
edgeSlope &= ~EDGE_SLOPE_ELEVATED;
}
}
auto targetLoc = CoordsXYZ(_loc, targetHeight);
rct_scenery_entry* wallEntry = get_wall_entry(_wallType);
if (wallEntry == nullptr)
{
log_error("Wall Type not found %d", _wallType);
return std::make_unique<WallPlaceActionResult>(GameActions::Status::InvalidParameters);
}
uint8_t clearanceHeight = targetHeight / COORDS_Z_STEP;
if (edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS))
{
clearanceHeight += 2;
}
clearanceHeight += wallEntry->wall.height;
bool wallAcrossTrack = false;
if (!(GetFlags() & GAME_COMMAND_FLAG_PATH_SCENERY) && !gCheatsDisableClearanceChecks)
{
auto result = WallCheckObstruction(wallEntry, targetHeight / COORDS_Z_STEP, clearanceHeight, &wallAcrossTrack);
if (result->Error != GameActions::Status::Ok)
{
return result;
}
}
if (!map_check_free_elements_and_reorganise(1))
{
return MakeResult(GameActions::Status::NoFreeElements, STR_TILE_ELEMENT_LIMIT_REACHED);
}
if (wallEntry->wall.scrolling_mode != SCROLLING_MODE_NONE)
{
if (_bannerId == BANNER_INDEX_NULL)
{
log_error("Banner Index not specified.");
return std::make_unique<WallPlaceActionResult>(
GameActions::Status::InvalidParameters, STR_TOO_MANY_BANNERS_IN_GAME);
}
auto banner = GetBanner(_bannerId);
if (!banner->IsNull())
{
log_error("No free banners available");
return std::make_unique<WallPlaceActionResult>(GameActions::Status::NoFreeElements);
}
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);
ride_id_t rideIndex = banner_get_closest_ride_index(targetLoc);
if (rideIndex != RIDE_ID_NULL)
{
banner->ride_index = rideIndex;
banner->flags |= BANNER_FLAG_LINKED_TO_RIDE;
}
}
TileElement* tileElement = tile_element_insert(targetLoc, 0b0000);
assert(tileElement != nullptr);
map_animation_create(MAP_ANIMATION_TYPE_WALL, targetLoc);
tileElement->SetType(TILE_ELEMENT_TYPE_WALL);
WallElement* wallElement = tileElement->AsWall();
wallElement->clearance_height = clearanceHeight;
wallElement->SetDirection(_edge);
wallElement->SetSlope(edgeSlope);
wallElement->SetPrimaryColour(_primaryColour);
wallElement->SetSecondaryColour(_secondaryColour);
if (wallAcrossTrack)
{
wallElement->SetAcrossTrack(true);
}
wallElement->SetEntryIndex(_wallType);
if (_bannerId != BANNER_INDEX_NULL)
{
wallElement->SetBannerIndex(_bannerId);
}
if (wallEntry->wall.flags & WALL_SCENERY_HAS_TERNARY_COLOUR)
{
wallElement->SetTertiaryColour(_tertiaryColour);
}
if (GetFlags() & GAME_COMMAND_FLAG_GHOST)
{
wallElement->SetGhost(true);
}
res->tileElement = tileElement;
map_invalidate_tile_zoom1({ _loc, wallElement->GetBaseZ(), wallElement->GetBaseZ() + 72 });
res->Cost = wallEntry->wall.price;
return res;
}
private:
/**
*
* rct2: 0x006E5CBA
*/
bool WallCheckObstructionWithTrack(rct_scenery_entry * wall, int32_t z0, TrackElement * trackElement, bool* wallAcrossTrack)
const
{
track_type_t trackType = trackElement->GetTrackType();
int32_t sequence = trackElement->GetSequenceIndex();
int32_t direction = (_edge - trackElement->GetDirection()) & TILE_ELEMENT_DIRECTION_MASK;
auto ride = get_ride(trackElement->GetRideIndex());
if (ride == nullptr)
{
return false;
}
if (TrackIsAllowedWallEdges(ride->type, trackType, sequence, direction))
{
return true;
}
if (!(wall->wall.flags & WALL_SCENERY_IS_DOOR))
{
return false;
}
if (!(RideTypeDescriptors[ride->type].Flags & RIDE_TYPE_FLAG_ALLOW_DOORS_ON_TRACK))
{
return false;
}
*wallAcrossTrack = true;
if (z0 & 1)
{
return false;
}
int32_t z;
if (sequence == 0)
{
if (TrackSequenceProperties[trackType][0] & TRACK_SEQUENCE_FLAG_DISALLOW_DOORS)
{
return false;
}
if (TrackDefinitions[trackType].bank_start == 0)
{
if (!(TrackCoordinates[trackType].rotation_begin & 4))
{
direction = direction_reverse(trackElement->GetDirection());
if (direction == _edge)
{
const rct_preview_track* trackBlock = &TrackBlocks[trackType][sequence];
z = TrackCoordinates[trackType].z_begin;
z = trackElement->base_height + ((z - trackBlock->z) * 8);
if (z == z0)
{
return true;
}
}
}
}
}
const rct_preview_track* trackBlock = &TrackBlocks[trackType][sequence + 1];
if (trackBlock->index != 0xFF)
{
return false;
}
if (TrackDefinitions[trackType].bank_end != 0)
{
return false;
}
direction = TrackCoordinates[trackType].rotation_end;
if (direction & 4)
{
return false;
}
direction = (trackElement->GetDirection() + TrackCoordinates[trackType].rotation_end) & TILE_ELEMENT_DIRECTION_MASK;
if (direction != _edge)
{
return false;
}
trackBlock = &TrackBlocks[trackType][sequence];
z = TrackCoordinates[trackType].z_end;
z = trackElement->base_height + ((z - trackBlock->z) * 8);
return z == z0;
}
/**
*
* rct2: 0x006E5C1A
*/
GameActions::Result::Ptr WallCheckObstruction(rct_scenery_entry * wall, int32_t z0, int32_t z1, bool* wallAcrossTrack) const
{
int32_t entryType, sequence;
rct_scenery_entry* entry;
rct_large_scenery_tile* tile;
*wallAcrossTrack = false;
gMapGroundFlags = ELEMENT_IS_ABOVE_GROUND;
if (map_is_location_at_edge(_loc))
{
return MakeResult(GameActions::Status::InvalidParameters, STR_OFF_EDGE_OF_MAP);
}
TileElement* tileElement = map_get_first_element_at(_loc);
do
{
if (tileElement == nullptr)
break;
int32_t elementType = tileElement->GetType();
if (elementType == TILE_ELEMENT_TYPE_SURFACE)
continue;
if (tileElement->IsGhost())
continue;
if (z0 >= tileElement->clearance_height)
continue;
if (z1 <= tileElement->base_height)
continue;
if (elementType == TILE_ELEMENT_TYPE_WALL)
{
int32_t direction = tileElement->GetDirection();
if (_edge == direction)
{
auto res = MakeResult(GameActions::Status::NoClearance, STR_NONE);
map_obstruction_set_error_text(tileElement, *res);
return res;
}
continue;
}
if (tileElement->GetOccupiedQuadrants() == 0)
continue;
auto res = MakeResult(GameActions::Status::NoClearance, STR_NONE);
switch (elementType)
{
case TILE_ELEMENT_TYPE_ENTRANCE:
map_obstruction_set_error_text(tileElement, *res);
return res;
case TILE_ELEMENT_TYPE_PATH:
if (tileElement->AsPath()->GetEdges() & (1 << _edge))
{
map_obstruction_set_error_text(tileElement, *res);
return res;
}
break;
case TILE_ELEMENT_TYPE_LARGE_SCENERY:
entryType = tileElement->AsLargeScenery()->GetEntryIndex();
sequence = tileElement->AsLargeScenery()->GetSequenceIndex();
entry = get_large_scenery_entry(entryType);
tile = &entry->large_scenery.tiles[sequence];
{
int32_t direction = ((_edge - tileElement->GetDirection()) & TILE_ELEMENT_DIRECTION_MASK) + 8;
if (!(tile->flags & (1 << direction)))
{
map_obstruction_set_error_text(tileElement, *res);
return res;
}
}
break;
case TILE_ELEMENT_TYPE_SMALL_SCENERY:
entry = tileElement->AsSmallScenery()->GetEntry();
if (scenery_small_entry_has_flag(entry, SMALL_SCENERY_FLAG_NO_WALLS))
{
map_obstruction_set_error_text(tileElement, *res);
return res;
}
break;
case TILE_ELEMENT_TYPE_TRACK:
if (!WallCheckObstructionWithTrack(wall, z0, tileElement->AsTrack(), wallAcrossTrack))
{
return res;
}
break;
}
} while (!(tileElement++)->IsLastForTile());
return MakeResult();
}
/**
* Gets whether the given track type can have a wall placed on the edge of the given direction.
* Some thin tracks for example are allowed to have walls either side of the track, but wider tracks can not.
*/
static bool TrackIsAllowedWallEdges(uint8_t rideType, uint8_t trackType, uint8_t trackSequence, uint8_t direction)
{
if (!ride_type_has_flag(rideType, RIDE_TYPE_FLAG_TRACK_NO_WALLS))
{
if (ride_type_has_flag(rideType, RIDE_TYPE_FLAG_FLAT_RIDE))
{
if (FlatRideTrackSequenceElementAllowedWallEdges[trackType][trackSequence] & (1 << direction))
{
return true;
}
}
else
{
if (TrackSequenceElementAllowedWallEdges[trackType][trackSequence] & (1 << direction))
{
return true;
}
}
}
return false;
}
};