mirror of https://github.com/OpenRCT2/OpenRCT2.git
458 lines
15 KiB
C++
458 lines
15 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 "SmallSceneryPlaceAction.h"
|
|
|
|
#include "../Cheats.h"
|
|
#include "../GameState.h"
|
|
#include "../OpenRCT2.h"
|
|
#include "../common.h"
|
|
#include "../core/MemoryStream.h"
|
|
#include "../interface/Window.h"
|
|
#include "../localisation/Localisation.h"
|
|
#include "../localisation/StringIds.h"
|
|
#include "../management/Finance.h"
|
|
#include "../object/ObjectEntryManager.h"
|
|
#include "../object/SmallSceneryEntry.h"
|
|
#include "../ride/Ride.h"
|
|
#include "../ride/TrackDesign.h"
|
|
#include "../world/ConstructionClearance.h"
|
|
#include "../world/MapAnimation.h"
|
|
#include "../world/Park.h"
|
|
#include "../world/Scenery.h"
|
|
#include "../world/Surface.h"
|
|
#include "../world/TileElement.h"
|
|
#include "GameAction.h"
|
|
#include "SmallSceneryRemoveAction.h"
|
|
|
|
using namespace OpenRCT2;
|
|
|
|
SmallSceneryPlaceAction::SmallSceneryPlaceAction(
|
|
const CoordsXYZD& loc, uint8_t quadrant, ObjectEntryIndex sceneryType, uint8_t primaryColour, uint8_t secondaryColour,
|
|
uint8_t tertiaryColour)
|
|
: _loc(loc)
|
|
, _quadrant(quadrant)
|
|
, _sceneryType(sceneryType)
|
|
, _primaryColour(primaryColour)
|
|
, _secondaryColour(secondaryColour)
|
|
, _tertiaryColour(tertiaryColour)
|
|
{
|
|
}
|
|
|
|
void SmallSceneryPlaceAction::AcceptParameters(GameActionParameterVisitor& visitor)
|
|
{
|
|
visitor.Visit(_loc);
|
|
visitor.Visit("quadrant", _quadrant);
|
|
visitor.Visit("object", _sceneryType);
|
|
visitor.Visit("primaryColour", _primaryColour);
|
|
visitor.Visit("secondaryColour", _secondaryColour);
|
|
visitor.Visit("tertiaryColour", _tertiaryColour);
|
|
}
|
|
|
|
uint32_t SmallSceneryPlaceAction::GetCooldownTime() const
|
|
{
|
|
return 20;
|
|
}
|
|
|
|
uint16_t SmallSceneryPlaceAction::GetActionFlags() const
|
|
{
|
|
return GameAction::GetActionFlags();
|
|
}
|
|
|
|
void SmallSceneryPlaceAction::Serialise(DataSerialiser& stream)
|
|
{
|
|
GameAction::Serialise(stream);
|
|
|
|
stream << DS_TAG(_loc) << DS_TAG(_quadrant) << DS_TAG(_sceneryType) << DS_TAG(_primaryColour) << DS_TAG(_secondaryColour)
|
|
<< DS_TAG(_tertiaryColour);
|
|
}
|
|
|
|
GameActions::Result SmallSceneryPlaceAction::Query() const
|
|
{
|
|
bool isOnWater = false;
|
|
bool supportsRequired = false;
|
|
if (_loc.z != 0)
|
|
{
|
|
supportsRequired = true;
|
|
}
|
|
int32_t landHeight = TileElementHeight(_loc);
|
|
int16_t waterHeight = TileElementWaterHeight(_loc);
|
|
|
|
int32_t surfaceHeight = landHeight;
|
|
// If on water
|
|
if (waterHeight > 0)
|
|
{
|
|
surfaceHeight = waterHeight;
|
|
}
|
|
auto res = GameActions::Result();
|
|
auto centre = _loc.ToTileCentre();
|
|
res.Position.x = centre.x;
|
|
res.Position.y = centre.y;
|
|
res.Position.z = surfaceHeight;
|
|
if (_loc.z != 0)
|
|
{
|
|
surfaceHeight = _loc.z;
|
|
res.Position.z = surfaceHeight;
|
|
}
|
|
|
|
if (!LocationValid(_loc))
|
|
{
|
|
return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_OFF_EDGE_OF_MAP);
|
|
}
|
|
|
|
if (!MapCheckCapacityAndReorganise(_loc))
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::NoFreeElements, STR_CANT_POSITION_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
|
|
}
|
|
|
|
auto maxSizeMax = GetMapSizeMaxXY();
|
|
if (!_trackDesignDrawingPreview && (_loc.x > maxSizeMax.x || _loc.y > maxSizeMax.y))
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_ERR_VALUE_OUT_OF_RANGE);
|
|
}
|
|
|
|
auto* sceneryEntry = ObjectManager::GetObjectEntry<SmallSceneryEntry>(_sceneryType);
|
|
if (sceneryEntry == nullptr)
|
|
{
|
|
LOG_ERROR("Small scenery object entry not found for sceneryType %u", _sceneryType);
|
|
return GameActions::Result(
|
|
GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
|
|
}
|
|
|
|
auto quadrant = _quadrant;
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE) || !sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_DIAGONAL))
|
|
{
|
|
if (sceneryEntry->HasFlag(
|
|
SMALL_SCENERY_FLAG_DIAGONAL | SMALL_SCENERY_FLAG_HALF_SPACE | SMALL_SCENERY_FLAG_THREE_QUARTERS))
|
|
{
|
|
quadrant = 0;
|
|
}
|
|
}
|
|
|
|
// Check if sub tile height is any different compared to actual surface tile height
|
|
auto loc2 = _loc;
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE))
|
|
{
|
|
loc2 = loc2.ToTileCentre();
|
|
}
|
|
else
|
|
{
|
|
loc2.x += SceneryQuadrantOffsets[quadrant & 3].x;
|
|
loc2.y += SceneryQuadrantOffsets[quadrant & 3].y;
|
|
}
|
|
landHeight = TileElementHeight(loc2);
|
|
waterHeight = TileElementWaterHeight(loc2);
|
|
|
|
surfaceHeight = landHeight;
|
|
// If on water
|
|
if (waterHeight > 0)
|
|
{
|
|
// BaseHeight2 is now the water height
|
|
surfaceHeight = waterHeight;
|
|
if (_loc.z == 0)
|
|
{
|
|
isOnWater = true;
|
|
}
|
|
}
|
|
auto targetHeight = _loc.z;
|
|
if (_loc.z == 0)
|
|
{
|
|
targetHeight = surfaceHeight;
|
|
}
|
|
|
|
if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !GetGameState().Cheats.SandboxMode
|
|
&& !MapIsLocationOwned({ _loc.x, _loc.y, targetHeight }))
|
|
{
|
|
return GameActions::Result(GameActions::Status::NotOwned, STR_CANT_POSITION_THIS_HERE, STR_LAND_NOT_OWNED_BY_PARK);
|
|
}
|
|
|
|
auto* surfaceElement = MapGetSurfaceElementAt(_loc);
|
|
|
|
if (surfaceElement != nullptr && !GetGameState().Cheats.DisableClearanceChecks && surfaceElement->GetWaterHeight() > 0)
|
|
{
|
|
int32_t water_height = surfaceElement->GetWaterHeight() - 1;
|
|
if (water_height > targetHeight)
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_CANT_BUILD_THIS_UNDERWATER);
|
|
}
|
|
}
|
|
|
|
if (!GetGameState().Cheats.DisableClearanceChecks && !(sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_STACKABLE)))
|
|
{
|
|
if (isOnWater)
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ON_LAND);
|
|
}
|
|
|
|
if (surfaceElement != nullptr && surfaceElement->GetWaterHeight() > 0)
|
|
{
|
|
if (surfaceElement->GetWaterHeight() > targetHeight)
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ON_LAND);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!GetGameState().Cheats.DisableClearanceChecks && (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_REQUIRE_FLAT_SURFACE))
|
|
&& !supportsRequired && !isOnWater && surfaceElement != nullptr
|
|
&& (surfaceElement->GetSlope() != TILE_ELEMENT_SLOPE_FLAT))
|
|
{
|
|
return GameActions::Result(GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_LEVEL_LAND_REQUIRED);
|
|
}
|
|
|
|
if (!GetGameState().Cheats.DisableSupportLimits && !(sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_STACKABLE))
|
|
&& supportsRequired)
|
|
{
|
|
if (!isOnWater)
|
|
{
|
|
if (surfaceElement != nullptr)
|
|
{
|
|
if (surfaceElement->GetWaterHeight() > 0 || (surfaceElement->GetBaseZ()) != targetHeight)
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_LEVEL_LAND_REQUIRED);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ON_LAND);
|
|
}
|
|
}
|
|
|
|
int32_t zLow = targetHeight;
|
|
int32_t zHigh = zLow + Ceil2(sceneryEntry->height, COORDS_Z_STEP);
|
|
uint8_t collisionQuadrants = 0b1111;
|
|
auto quadRotation{ 0 };
|
|
if (!(sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE)))
|
|
{
|
|
quadRotation = (quadrant ^ 2);
|
|
collisionQuadrants = 0b0001;
|
|
}
|
|
if (!(sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_HALF_SPACE)))
|
|
{
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_DIAGONAL) && sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE))
|
|
{
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_THREE_QUARTERS))
|
|
{
|
|
quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
|
|
collisionQuadrants = 0b1011;
|
|
}
|
|
else
|
|
{
|
|
quadRotation = (quadrant + _loc.direction) & 1;
|
|
collisionQuadrants = 0b1010;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
|
|
collisionQuadrants = 0b0011;
|
|
}
|
|
uint8_t supports = 0;
|
|
if (!supportsRequired)
|
|
{
|
|
supports = 0b1111;
|
|
}
|
|
|
|
QuarterTile quarterTile = QuarterTile{ collisionQuadrants, supports }.Rotate(quadRotation);
|
|
const auto isTree = sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_IS_TREE);
|
|
auto canBuild = MapCanConstructWithClearAt(
|
|
{ _loc, zLow, zHigh }, &MapPlaceSceneryClearFunc, quarterTile, GetFlags(), CREATE_CROSSING_MODE_NONE, isTree);
|
|
if (canBuild.Error != GameActions::Status::Ok)
|
|
{
|
|
canBuild.ErrorTitle = STR_CANT_POSITION_THIS_HERE;
|
|
return canBuild;
|
|
}
|
|
|
|
const auto clearanceData = canBuild.GetData<ConstructClearResult>();
|
|
const uint8_t groundFlags = clearanceData.GroundFlags & (ELEMENT_IS_ABOVE_GROUND | ELEMENT_IS_UNDERGROUND);
|
|
res.SetData(SmallSceneryPlaceActionResult{ groundFlags, 0, 0 });
|
|
|
|
res.Expenditure = ExpenditureType::Landscaping;
|
|
res.Cost = sceneryEntry->price + canBuild.Cost;
|
|
|
|
return res;
|
|
}
|
|
|
|
GameActions::Result SmallSceneryPlaceAction::Execute() const
|
|
{
|
|
bool supportsRequired = false;
|
|
if (_loc.z != 0)
|
|
{
|
|
supportsRequired = true;
|
|
}
|
|
int32_t landHeight = TileElementHeight(_loc);
|
|
int16_t waterHeight = TileElementWaterHeight(_loc);
|
|
|
|
int32_t surfaceHeight = landHeight;
|
|
// If on water
|
|
if (waterHeight > 0)
|
|
{
|
|
surfaceHeight = waterHeight;
|
|
}
|
|
auto res = GameActions::Result();
|
|
auto centre = _loc.ToTileCentre();
|
|
res.Position.x = centre.x;
|
|
res.Position.y = centre.y;
|
|
res.Position.z = surfaceHeight;
|
|
if (_loc.z != 0)
|
|
{
|
|
surfaceHeight = _loc.z;
|
|
res.Position.z = surfaceHeight;
|
|
}
|
|
|
|
auto* sceneryEntry = ObjectManager::GetObjectEntry<SmallSceneryEntry>(_sceneryType);
|
|
if (sceneryEntry == nullptr)
|
|
{
|
|
LOG_ERROR("Small scenery object entry not found for sceneryType %u", _sceneryType);
|
|
return GameActions::Result(
|
|
GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
|
|
}
|
|
|
|
auto quadrant = _quadrant;
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE) || !sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_DIAGONAL))
|
|
{
|
|
if (sceneryEntry->HasFlag(
|
|
SMALL_SCENERY_FLAG_DIAGONAL | SMALL_SCENERY_FLAG_HALF_SPACE | SMALL_SCENERY_FLAG_THREE_QUARTERS))
|
|
{
|
|
quadrant = 0;
|
|
}
|
|
}
|
|
|
|
// Check if sub tile height is any different compared to actual surface tile height
|
|
int32_t x2 = _loc.x;
|
|
int32_t y2 = _loc.y;
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE))
|
|
{
|
|
x2 += 16;
|
|
y2 += 16;
|
|
}
|
|
else
|
|
{
|
|
x2 += SceneryQuadrantOffsets[quadrant & 3].x;
|
|
y2 += SceneryQuadrantOffsets[quadrant & 3].y;
|
|
}
|
|
landHeight = TileElementHeight({ x2, y2 });
|
|
waterHeight = TileElementWaterHeight({ x2, y2 });
|
|
|
|
surfaceHeight = landHeight;
|
|
// If on water
|
|
if (waterHeight > 0)
|
|
{
|
|
// BaseHeight2 is now the water height
|
|
surfaceHeight = waterHeight;
|
|
}
|
|
auto targetHeight = _loc.z;
|
|
if (_loc.z == 0)
|
|
{
|
|
targetHeight = surfaceHeight;
|
|
}
|
|
|
|
if (!(GetFlags() & GAME_COMMAND_FLAG_GHOST))
|
|
{
|
|
FootpathRemoveLitter({ _loc, targetHeight });
|
|
if (!GetGameState().Cheats.DisableClearanceChecks && (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_NO_WALLS)))
|
|
{
|
|
WallRemoveAt({ _loc, targetHeight, targetHeight + sceneryEntry->height });
|
|
}
|
|
}
|
|
|
|
int32_t zLow = targetHeight;
|
|
int32_t zHigh = zLow + Ceil2(sceneryEntry->height, 8);
|
|
uint8_t collisionQuadrants = 0b1111;
|
|
auto quadRotation{ 0 };
|
|
if (!(sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE)))
|
|
{
|
|
quadRotation = (quadrant ^ 2);
|
|
collisionQuadrants = 0b0001;
|
|
}
|
|
if (!(sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_HALF_SPACE)))
|
|
{
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_DIAGONAL) && sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE))
|
|
{
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_THREE_QUARTERS))
|
|
{
|
|
quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
|
|
collisionQuadrants = 0b1011;
|
|
}
|
|
else
|
|
{
|
|
quadRotation = (quadrant + _loc.direction) & 1;
|
|
collisionQuadrants = 0b1010;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
|
|
collisionQuadrants = 0b0011;
|
|
}
|
|
uint8_t supports = 0;
|
|
if (!supportsRequired)
|
|
{
|
|
supports = 0b1111;
|
|
}
|
|
|
|
QuarterTile quarterTile = QuarterTile{ collisionQuadrants, supports }.Rotate(quadRotation);
|
|
const auto isTree = sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_IS_TREE);
|
|
auto canBuild = MapCanConstructWithClearAt(
|
|
{ _loc, zLow, zHigh }, &MapPlaceSceneryClearFunc, quarterTile, GetFlags() | GAME_COMMAND_FLAG_APPLY,
|
|
CREATE_CROSSING_MODE_NONE, isTree);
|
|
if (canBuild.Error != GameActions::Status::Ok)
|
|
{
|
|
canBuild.ErrorTitle = STR_CANT_POSITION_THIS_HERE;
|
|
return canBuild;
|
|
}
|
|
|
|
res.Expenditure = ExpenditureType::Landscaping;
|
|
res.Cost = sceneryEntry->price + canBuild.Cost;
|
|
|
|
auto* sceneryElement = TileElementInsert<SmallSceneryElement>(
|
|
CoordsXYZ{ _loc, zLow }, quarterTile.GetBaseQuarterOccupied());
|
|
if (sceneryElement == nullptr)
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::NoFreeElements, STR_CANT_POSITION_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
|
|
}
|
|
|
|
sceneryElement->SetDirection(_loc.direction);
|
|
sceneryElement->SetSceneryQuadrant(quadrant);
|
|
sceneryElement->SetEntryIndex(_sceneryType);
|
|
sceneryElement->SetAge(0);
|
|
sceneryElement->SetPrimaryColour(_primaryColour);
|
|
sceneryElement->SetSecondaryColour(_secondaryColour);
|
|
sceneryElement->SetTertiaryColour(_tertiaryColour);
|
|
sceneryElement->SetClearanceZ(sceneryElement->GetBaseZ() + sceneryEntry->height + 7);
|
|
sceneryElement->SetGhost(GetFlags() & GAME_COMMAND_FLAG_GHOST);
|
|
if (supportsRequired)
|
|
{
|
|
sceneryElement->SetNeedsSupports();
|
|
}
|
|
|
|
const auto clearanceData = canBuild.GetData<ConstructClearResult>();
|
|
const uint8_t groundFlags = clearanceData.GroundFlags & (ELEMENT_IS_ABOVE_GROUND | ELEMENT_IS_UNDERGROUND);
|
|
res.SetData(SmallSceneryPlaceActionResult{ groundFlags, sceneryElement->GetBaseZ(), sceneryElement->GetSceneryQuadrant() });
|
|
|
|
MapInvalidateTileFull(_loc);
|
|
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_ANIMATED))
|
|
{
|
|
MapAnimationCreate(MAP_ANIMATION_TYPE_SMALL_SCENERY, CoordsXYZ{ _loc, sceneryElement->GetBaseZ() });
|
|
}
|
|
|
|
return res;
|
|
}
|