OpenRCT2/src/openrct2/ride/TrackDesign.cpp

2131 lines
72 KiB
C++
Raw Normal View History

2016-05-04 19:54:33 +02:00
/*****************************************************************************
* Copyright (c) 2014-2024 OpenRCT2 developers
2016-05-04 19:54:33 +02:00
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
2016-05-04 19:54:33 +02:00
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
2016-05-04 19:54:33 +02:00
*****************************************************************************/
2018-06-22 23:14:18 +02:00
#include "TrackDesign.h"
#include "../Cheats.h"
Split actions hpp files into separate h and cpp files (#13548) * Split up SmallSceneryPlace/Remove Added undo function for Remove Scenery * Refactor: Balloon and Banner actions hpp=>h/cpp * Refactor: rename all action *.hpp files to *.cpp This is preparation for separation in later commits. Note that without the complete set of commits in this branch, the code will not build. * Refactor Clear, Climate, Custom, and Footpath actions hpp=>h/cpp * VSCode: add src subdirectories to includePath * Refactor Guest actions hpp=>h/cpp * Refactor Land actions hpp=>h/cpp * Refactor LargeScenery actions hpp=>h/cpp * Refactor Load, Maze, Network actions hpp=>h/cpp * Refactor Park actions hpp=>h/cpp * Refactor/style: move private function declarations in actions *.h Previous action .h files included private function declarations with private member variables, before public function declarations. This commit re-orders the header files to the following order: - public member variables - private member variables - public functions - private functions * Refactor Pause action hpp=>h/cpp * Refactor Peep, Place, Player actions hpp=>h/cpp * Refactor Ride actions hpp=>h/cpp * Refactor Scenario, Set*, Sign* actions hpp=>h/cpp * Refactor SmallScenerySetColourAction hpp=>h/cpp * Refactor Staff actions hpp=>h/cpp * Refactor Surface, Tile, Track* actions hpp=>h/cpp * Refactor Wall and Water actions hpp=>h/cpp * Fix various includes and other compile errors Update includes for tests. Move static function declarations to .h files Add explicit includes to various files that were previously implicit (the required header was a nested include in an action hpp file, and the action .h file does not include that header) Move RideSetStatus string enum to the cpp file to avoid unused imports * Xcode: modify project file for actions refactor * Cleanup whitespace and end-of-file newlines Co-authored-by: duncanspumpkin <duncans_pumpkin@hotmail.co.uk>
2020-12-10 07:39:10 +01:00
#include "../Context.h"
2018-06-22 23:14:18 +02:00
#include "../Game.h"
#include "../GameState.h"
2018-06-22 23:14:18 +02:00
#include "../OpenRCT2.h"
2019-04-08 20:34:24 +02:00
#include "../TrackImporter.h"
#include "../actions/FootpathLayoutPlaceAction.h"
Split actions hpp files into separate h and cpp files (#13548) * Split up SmallSceneryPlace/Remove Added undo function for Remove Scenery * Refactor: Balloon and Banner actions hpp=>h/cpp * Refactor: rename all action *.hpp files to *.cpp This is preparation for separation in later commits. Note that without the complete set of commits in this branch, the code will not build. * Refactor Clear, Climate, Custom, and Footpath actions hpp=>h/cpp * VSCode: add src subdirectories to includePath * Refactor Guest actions hpp=>h/cpp * Refactor Land actions hpp=>h/cpp * Refactor LargeScenery actions hpp=>h/cpp * Refactor Load, Maze, Network actions hpp=>h/cpp * Refactor Park actions hpp=>h/cpp * Refactor/style: move private function declarations in actions *.h Previous action .h files included private function declarations with private member variables, before public function declarations. This commit re-orders the header files to the following order: - public member variables - private member variables - public functions - private functions * Refactor Pause action hpp=>h/cpp * Refactor Peep, Place, Player actions hpp=>h/cpp * Refactor Ride actions hpp=>h/cpp * Refactor Scenario, Set*, Sign* actions hpp=>h/cpp * Refactor SmallScenerySetColourAction hpp=>h/cpp * Refactor Staff actions hpp=>h/cpp * Refactor Surface, Tile, Track* actions hpp=>h/cpp * Refactor Wall and Water actions hpp=>h/cpp * Fix various includes and other compile errors Update includes for tests. Move static function declarations to .h files Add explicit includes to various files that were previously implicit (the required header was a nested include in an action hpp file, and the action .h file does not include that header) Move RideSetStatus string enum to the cpp file to avoid unused imports * Xcode: modify project file for actions refactor * Cleanup whitespace and end-of-file newlines Co-authored-by: duncanspumpkin <duncans_pumpkin@hotmail.co.uk>
2020-12-10 07:39:10 +01:00
#include "../actions/FootpathRemoveAction.h"
#include "../actions/LargeSceneryPlaceAction.h"
#include "../actions/LargeSceneryRemoveAction.h"
#include "../actions/MazePlaceTrackAction.h"
#include "../actions/RideCreateAction.h"
2022-08-11 00:00:58 +02:00
#include "../actions/RideDemolishAction.h"
Split actions hpp files into separate h and cpp files (#13548) * Split up SmallSceneryPlace/Remove Added undo function for Remove Scenery * Refactor: Balloon and Banner actions hpp=>h/cpp * Refactor: rename all action *.hpp files to *.cpp This is preparation for separation in later commits. Note that without the complete set of commits in this branch, the code will not build. * Refactor Clear, Climate, Custom, and Footpath actions hpp=>h/cpp * VSCode: add src subdirectories to includePath * Refactor Guest actions hpp=>h/cpp * Refactor Land actions hpp=>h/cpp * Refactor LargeScenery actions hpp=>h/cpp * Refactor Load, Maze, Network actions hpp=>h/cpp * Refactor Park actions hpp=>h/cpp * Refactor/style: move private function declarations in actions *.h Previous action .h files included private function declarations with private member variables, before public function declarations. This commit re-orders the header files to the following order: - public member variables - private member variables - public functions - private functions * Refactor Pause action hpp=>h/cpp * Refactor Peep, Place, Player actions hpp=>h/cpp * Refactor Ride actions hpp=>h/cpp * Refactor Scenario, Set*, Sign* actions hpp=>h/cpp * Refactor SmallScenerySetColourAction hpp=>h/cpp * Refactor Staff actions hpp=>h/cpp * Refactor Surface, Tile, Track* actions hpp=>h/cpp * Refactor Wall and Water actions hpp=>h/cpp * Fix various includes and other compile errors Update includes for tests. Move static function declarations to .h files Add explicit includes to various files that were previously implicit (the required header was a nested include in an action hpp file, and the action .h file does not include that header) Move RideSetStatus string enum to the cpp file to avoid unused imports * Xcode: modify project file for actions refactor * Cleanup whitespace and end-of-file newlines Co-authored-by: duncanspumpkin <duncans_pumpkin@hotmail.co.uk>
2020-12-10 07:39:10 +01:00
#include "../actions/RideEntranceExitPlaceAction.h"
#include "../actions/SmallSceneryPlaceAction.h"
#include "../actions/SmallSceneryRemoveAction.h"
#include "../actions/TrackPlaceAction.h"
#include "../actions/TrackRemoveAction.h"
#include "../actions/WallPlaceAction.h"
#include "../actions/WallRemoveAction.h"
2018-06-22 23:14:18 +02:00
#include "../audio/audio.h"
#include "../core/DataSerialiser.h"
#include "../core/File.h"
#include "../core/Numerics.hpp"
#include "../core/String.hpp"
#include "../drawing/X8DrawingEngine.h"
#include "../localisation/Localisation.h"
#include "../localisation/StringIds.h"
#include "../management/Finance.h"
#include "../network/network.h"
#include "../object/FootpathObject.h"
#include "../object/FootpathSurfaceObject.h"
2023-01-26 19:44:42 +01:00
#include "../object/LargeSceneryEntry.h"
#include "../object/ObjectEntryManager.h"
2018-02-13 15:20:55 +01:00
#include "../object/ObjectList.h"
2016-07-07 01:09:31 +02:00
#include "../object/ObjectManager.h"
#include "../object/ObjectRepository.h"
#include "../object/SmallSceneryEntry.h"
#include "../object/StationObject.h"
#include "../rct2/RCT2.h"
2021-12-18 19:50:29 +01:00
#include "../ride/RideConstruction.h"
2017-12-13 13:02:24 +01:00
#include "../util/SawyerCoding.h"
#include "../util/Util.h"
2018-01-11 10:59:26 +01:00
#include "../world/Footpath.h"
2018-03-19 23:28:40 +01:00
#include "../world/Park.h"
2018-01-11 10:59:26 +01:00
#include "../world/Scenery.h"
2018-05-01 16:33:16 +02:00
#include "../world/Surface.h"
#include "../world/Wall.h"
2018-06-22 23:14:18 +02:00
#include "Ride.h"
#include "RideData.h"
#include "Track.h"
#include "TrackData.h"
2021-09-08 23:06:34 +02:00
#include "TrackDesign.h"
2018-06-22 23:14:18 +02:00
#include "TrackDesignRepository.h"
2021-02-22 22:13:06 +01:00
#include "Vehicle.h"
#include <algorithm>
2018-11-21 23:16:04 +01:00
#include <iterator>
#include <memory>
using namespace OpenRCT2;
using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::TrackMetaData;
2022-02-09 21:48:22 +01:00
constexpr TileCoordsXY TRACK_DESIGN_PREVIEW_MAP_SIZE = TileCoordsXY{ 256, 256 };
2018-06-22 23:14:18 +02:00
bool gTrackDesignSceneryToggle;
2021-08-08 18:10:52 +02:00
bool _trackDesignDrawingPreview;
bool _trackDesignPlaceStateSceneryUnavailable = false;
static bool _trackDesignPlaceStateEntranceExitPlaced{};
static void TrackDesignPreviewClearMap();
static u8string_view TrackDesignGetStationObjectIdentifier(const Ride& ride)
{
const auto* stationObject = ride.GetStationObject();
if (stationObject == nullptr)
return "";
return stationObject->GetIdentifier();
}
ResultWithMessage TrackDesign::CreateTrackDesign(TrackDesignState& tds, const Ride& ride)
2019-06-30 12:23:06 +02:00
{
type = ride.type;
auto object = ObjectEntryGetObject(ObjectType::Ride, ride.subtype);
if (object != nullptr)
{
auto entry = object->GetObjectEntry();
// Remove this check for new track design format
if (entry.IsEmpty())
{
return { false, STR_VEHICLE_UNSUPPORTED_TD6 };
}
vehicle_object = ObjectEntryDescriptor(entry);
}
2019-06-30 12:23:06 +02:00
ride_mode = ride.mode;
colour_scheme = ride.colour_scheme_type & 3;
for (size_t i = 0; i < std::size(vehicle_colours); i++)
2019-06-30 12:23:06 +02:00
{
vehicle_colours[i] = ride.vehicle_colours[i];
2019-06-30 12:23:06 +02:00
}
for (int32_t i = 0; i < OpenRCT2::Limits::NumColourSchemes; i++)
2019-06-30 12:23:06 +02:00
{
track_spine_colour[i] = ride.track_colour[i].main;
track_rail_colour[i] = ride.track_colour[i].additional;
track_support_colour[i] = ride.track_colour[i].supports;
}
depart_flags = ride.depart_flags;
number_of_trains = ride.NumTrains;
2019-06-30 12:23:06 +02:00
number_of_cars_per_train = ride.num_cars_per_train;
min_waiting_time = ride.min_waiting_time;
max_waiting_time = ride.max_waiting_time;
operation_setting = ride.operation_option;
lift_hill_speed = ride.lift_hill_speed;
num_circuits = ride.num_circuits;
StationObjectIdentifier = TrackDesignGetStationObjectIdentifier(ride);
max_speed = static_cast<int8_t>(ride.max_speed / 65536);
average_speed = static_cast<int8_t>(ride.average_speed / 65536);
2021-12-12 12:26:42 +01:00
ride_length = ride.GetTotalLength() / 65536;
2019-06-30 12:23:06 +02:00
max_positive_vertical_g = ride.max_positive_vertical_g / 32;
max_negative_vertical_g = ride.max_negative_vertical_g / 32;
max_lateral_g = ride.max_lateral_g / 32;
holes = ride.holes & 0x1F;
2019-06-30 12:23:06 +02:00
inversions = ride.inversions & 0x1F;
inversions |= (ride.sheltered_eighths << 5);
drops = ride.drops;
highest_drop_height = ride.highest_drop_height;
uint16_t totalAirTime = (ride.total_air_time * 123) / 1024;
if (totalAirTime > 255)
{
totalAirTime = 0;
}
total_air_time = static_cast<uint8_t>(totalAirTime);
2019-06-30 12:23:06 +02:00
excitement = ride.ratings.Excitement / 10;
intensity = ride.ratings.Intensity / 10;
nausea = ride.ratings.Nausea / 10;
2019-06-30 12:23:06 +02:00
upkeep_cost = ride.upkeep_cost;
flags = 0;
flags2 = 0;
const auto& rtd = GetRideTypeDescriptor(type);
if (rtd.DesignCreateMode == TrackDesignCreateMode::Maze)
2019-06-30 12:23:06 +02:00
{
return CreateTrackDesignMaze(tds, ride);
2019-06-30 12:23:06 +02:00
}
else
{
return CreateTrackDesignTrack(tds, ride);
}
2019-06-30 12:23:06 +02:00
}
ResultWithMessage TrackDesign::CreateTrackDesignTrack(TrackDesignState& tds, const Ride& ride)
2019-06-30 12:23:06 +02:00
{
CoordsXYE trackElement;
if (!RideTryGetOriginElement(ride, &trackElement))
2019-06-30 12:23:06 +02:00
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
StringId warningMessage = STR_NONE;
RideGetStartOfTrack(&trackElement);
2019-06-30 12:23:06 +02:00
int32_t z = trackElement.element->GetBaseZ();
auto trackType = trackElement.element->AsTrack()->GetTrackType();
2019-06-30 12:23:06 +02:00
uint8_t direction = trackElement.element->GetDirection();
_saveDirection = direction;
auto newCoords = GetTrackElementOriginAndApplyChanges(
{ trackElement, z, direction }, trackType, 0, &trackElement.element, 0);
2019-06-30 12:23:06 +02:00
2021-09-13 18:47:13 +02:00
if (!newCoords.has_value())
2019-06-30 12:23:06 +02:00
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
trackElement.x = newCoords->x;
trackElement.y = newCoords->y;
z = newCoords->z;
2019-06-30 12:23:06 +02:00
2021-08-27 23:49:32 +02:00
const auto& ted = GetTrackElementDescriptor(trackElement.element->AsTrack()->GetTrackType());
const TrackCoordinates* trackCoordinates = &ted.Coordinates;
2021-08-27 23:49:32 +02:00
const auto* trackBlock = ted.Block;
2019-06-30 12:23:06 +02:00
// Used in the following loop to know when we have
// completed all of the elements and are back at the
// start.
TileElement* initialMap = trackElement.element;
CoordsXYZ startPos = { trackElement.x, trackElement.y, z + trackCoordinates->z_begin - trackBlock->z };
2021-10-28 22:11:43 +02:00
tds.Origin = startPos;
2019-06-30 12:23:06 +02:00
do
{
const auto& element = trackElement.element->AsTrack();
// Remove this check for new track design format
if (element->GetTrackType() > TrackElemType::HighestAlias)
{
return { false, STR_TRACK_ELEM_UNSUPPORTED_TD6 };
}
TrackDesignTrackElement track{};
track.Type = element->GetTrackType();
track.ColourScheme = element->GetColourScheme();
track.StationIndex = element->GetStationIndex();
track.BrakeBoosterSpeed = element->GetBrakeBoosterSpeed();
track.SeatRotation = element->GetSeatRotation();
2019-06-30 12:23:06 +02:00
// This warning will not apply to new track design format
if (track.Type == TrackElemType::BlockBrakes && element->GetBrakeBoosterSpeed() != kRCT2DefaultBlockBrakeSpeed)
{
warningMessage = STR_TRACK_DESIGN_BLOCK_BRAKE_SPEED_RESET;
}
if (element->HasChain())
track.SetFlag(TrackDesignTrackElementFlag::HasChain);
if (ride.GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_ALTERNATIVE_TRACK_TYPE) && element->IsInverted())
2019-06-30 12:23:06 +02:00
{
track.SetFlag(TrackDesignTrackElementFlag::IsInverted);
2019-06-30 12:23:06 +02:00
}
track_elements.push_back(track);
if (!TrackBlockGetNext(&trackElement, &trackElement, nullptr, nullptr))
2019-06-30 12:23:06 +02:00
{
break;
}
z = trackElement.element->GetBaseZ();
2019-06-30 12:23:06 +02:00
direction = trackElement.element->GetDirection();
trackType = trackElement.element->AsTrack()->GetTrackType();
newCoords = GetTrackElementOriginAndApplyChanges(
{ trackElement, z, direction }, trackType, 0, &trackElement.element, 0);
2019-06-30 12:23:06 +02:00
2021-09-13 18:47:13 +02:00
if (!newCoords.has_value())
2019-06-30 12:23:06 +02:00
{
break;
}
trackElement.x = newCoords->x;
trackElement.y = newCoords->y;
2019-06-30 12:23:06 +02:00
if (track_elements.size() > RCT2::Limits::TD6MaxTrackElements)
2019-06-30 12:23:06 +02:00
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
} while (trackElement.element != initialMap);
// First entrances, second exits
for (int32_t i = 0; i < 2; i++)
{
for (const auto& station : ride.GetStations())
2019-06-30 12:23:06 +02:00
{
z = station.GetBaseZ();
2019-06-30 12:23:06 +02:00
TileCoordsXYZD location;
if (i == 0)
{
location = station.Entrance;
2019-06-30 12:23:06 +02:00
}
else
{
location = station.Exit;
2019-06-30 12:23:06 +02:00
}
if (location.IsNull())
2019-06-30 12:23:06 +02:00
{
continue;
}
CoordsXY mapLocation = location.ToCoordsXY();
2019-06-30 12:23:06 +02:00
TileElement* tileElement = MapGetFirstElementAt(mapLocation);
if (tileElement == nullptr)
continue;
2019-06-30 12:23:06 +02:00
do
{
2021-12-11 00:39:39 +01:00
if (tileElement->GetType() != TileElementType::Entrance)
2019-06-30 12:23:06 +02:00
continue;
if (tileElement->GetBaseZ() == z)
2019-06-30 12:23:06 +02:00
break;
} while (!(tileElement++)->IsLastForTile());
// Add something that stops this from walking off the end
Direction entranceDirection = tileElement->GetDirection();
entranceDirection -= _saveDirection;
entranceDirection &= kTileElementDirectionMask;
2019-06-30 12:23:06 +02:00
2021-10-28 22:11:43 +02:00
mapLocation -= tds.Origin;
2019-06-30 12:23:06 +02:00
// Rotate entrance coordinates backwards to the correct direction
auto rotatedMapLocation = TileCoordsXY(mapLocation.Rotate(0 - _saveDirection));
2019-06-30 12:23:06 +02:00
2021-10-28 22:11:43 +02:00
z -= tds.Origin.z;
z /= COORDS_Z_STEP;
2019-06-30 12:23:06 +02:00
if (z > 127 || z < -126)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
TrackDesignEntranceElement entrance{};
entrance.Location = TileCoordsXYZD(rotatedMapLocation, z, entranceDirection);
2019-06-30 12:23:06 +02:00
// If this is the exit version
if (i == 1)
{
entrance.IsExit = true;
2019-06-30 12:23:06 +02:00
}
entrance_elements.push_back(entrance);
}
}
TrackDesignPreviewDrawOutlines(tds, this, RideGetTemporaryForPreview(), { 4096, 4096, 0, _currentTrackPieceDirection });
2019-06-30 12:23:06 +02:00
// Resave global vars for scenery reasons.
2021-10-28 22:11:43 +02:00
tds.Origin = startPos;
2019-06-30 12:23:06 +02:00
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_CONSTRUCT;
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_ARROW;
gMapSelectFlags &= ~MAP_SELECT_FLAG_GREEN;
2021-10-28 22:11:43 +02:00
space_required_x = ((tds.PreviewMax.x - tds.PreviewMin.x) / 32) + 1;
space_required_y = ((tds.PreviewMax.y - tds.PreviewMin.y) / 32) + 1;
return { true, warningMessage };
2019-06-30 12:23:06 +02:00
}
ResultWithMessage TrackDesign::CreateTrackDesignMaze(TrackDesignState& tds, const Ride& ride)
2019-06-30 12:23:06 +02:00
{
auto startLoc = MazeGetFirstElement(ride);
if (startLoc.element == nullptr)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
2021-10-28 22:11:43 +02:00
tds.Origin = { startLoc.x, startLoc.y, startLoc.element->GetBaseZ() };
2019-06-30 12:23:06 +02:00
// x is defined here as we can start the search
// on tile start_x, start_y but then the next row
// must restart on 0
for (int32_t y = startLoc.y, x = startLoc.x; y < MAXIMUM_MAP_SIZE_BIG; y += COORDS_XY_STEP)
2019-06-30 12:23:06 +02:00
{
for (; x < MAXIMUM_MAP_SIZE_BIG; x += COORDS_XY_STEP)
2019-06-30 12:23:06 +02:00
{
auto tileElement = MapGetFirstElementAt(CoordsXY{ x, y });
2019-06-30 12:23:06 +02:00
do
{
if (tileElement == nullptr)
break;
2021-12-11 00:39:39 +01:00
if (tileElement->GetType() != TileElementType::Track)
2019-06-30 12:23:06 +02:00
continue;
if (tileElement->AsTrack()->GetRideIndex() != ride.id)
continue;
TrackDesignMazeElement maze{};
2019-06-30 12:23:06 +02:00
maze.maze_entry = tileElement->AsTrack()->GetMazeEntry();
2020-03-13 12:03:43 +01:00
maze.x = (x - startLoc.x) / COORDS_XY_STEP;
maze.y = (y - startLoc.y) / COORDS_XY_STEP;
2019-06-30 12:23:06 +02:00
_saveDirection = tileElement->GetDirection();
maze_elements.push_back(maze);
if (maze_elements.size() >= 2000)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
} while (!(tileElement++)->IsLastForTile());
}
x = 0;
}
auto location = ride.GetStation().Entrance;
if (location.IsNull())
2019-06-30 12:23:06 +02:00
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
CoordsXY entranceLoc = location.ToCoordsXY();
auto tileElement = MapGetFirstElementAt(entranceLoc);
2019-06-30 12:23:06 +02:00
do
{
if (tileElement == nullptr)
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2021-12-11 00:39:39 +01:00
if (tileElement->GetType() != TileElementType::Entrance)
2019-06-30 12:23:06 +02:00
continue;
if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_ENTRANCE)
continue;
if (tileElement->AsEntrance()->GetRideIndex() == ride.id)
break;
} while (!(tileElement++)->IsLastForTile());
// Add something that stops this from walking off the end
auto entranceOffset = entranceLoc - startLoc;
TrackDesignEntranceElement mazeEntrance{};
mazeEntrance.Location = TileCoordsXYZD(CoordsXYZD(entranceOffset, 0, tileElement->GetDirection()));
mazeEntrance.IsExit = false;
entrance_elements.push_back(mazeEntrance);
2019-06-30 12:23:06 +02:00
location = ride.GetStation().Exit;
if (location.IsNull())
2019-06-30 12:23:06 +02:00
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
CoordsXY exitLoc = location.ToCoordsXY();
tileElement = MapGetFirstElementAt(exitLoc);
Avoid dereferencing map_get_first_element_at nullptr on libopenrct2 (#10013) * Avoid dereferencing map_get_first_element_at nullptr on Map.cpp * Avoid dereferencing map_get_first_element_at nullptr on MapAnimation.cpp Returning true or internal control variable, based on what was seen on `map_animation_invalidate_track_onridephoto` * Avoid dereferencing map_get_first_element_at nullptr on Park.cpp * Avoid dereferencing map_get_first_element_at nullptr on Scenery.cpp * Avoid dereferencing map_get_first_element_at nullptr on Sprite.cpp * Avoid dereferencing map_get_first_element_at nullptr on TileInspector.cpp * Avoid dereferencing map_get_first_element_at nullptr on Wall.cpp * Avoid dereferencing map_get_first_element_at nullptr on Fountain.cpp * Avoid dereferencing map_get_first_element_at nullptr on Footpath.cpp * Avoid dereferencing map_get_first_element_at nullptr on Entrance.cpp * Avoid dereferencing map_get_first_element_at nullptr on Banner.cpp * Avoid dereferencing map_get_first_element_at nullptr on Vehicle.cpp * Avoid dereferencing map_get_first_element_at nullptr on TrackDesignSave.cpp * Avoid dereferencing map_get_first_element_at nullptr on TrackDesign.cpp * Avoid dereferencing map_get_first_element_at nullptr on Track.cpp * Avoid dereferencing map_get_first_element_at nullptr on Station.cpp * Avoid dereferencing map_get_first_element_at nullptr on RideRatings.cpp * Avoid dereferencing map_get_first_element_at nullptr on Ride.cpp * Avoid dereferencing map_get_first_element_at nullptr on S4Importer.cpp * Avoid dereferencing map_get_first_element_at nullptr on Staff.cpp * Avoid dereferencing map_get_first_element_at nullptr on Peep.cpp * Avoid dereferencing map_get_first_element_at nullptr on GuestPathfinding.cpp * Avoid dereferencing map_get_first_element_at nullptr on Guest.cpp * Avoid dereferencing map_get_first_element_at nullptr on VirtualFloor.cpp * Avoid dereferencing map_get_first_element_at nullptr on Paint.TileElement.cpp * Fix issues raised on review * Fix remaining review issues. * Early exit on loops if tileElement is nullptr * Fix clang-format issues
2019-10-09 16:02:21 +02:00
if (tileElement == nullptr)
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
do
{
2021-12-11 00:39:39 +01:00
if (tileElement->GetType() != TileElementType::Entrance)
2019-06-30 12:23:06 +02:00
continue;
if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_EXIT)
continue;
if (tileElement->AsEntrance()->GetRideIndex() == ride.id)
break;
} while (!(tileElement++)->IsLastForTile());
// Add something that stops this from walking off the end
auto exitOffset = exitLoc - startLoc;
TrackDesignEntranceElement mazeExit{};
mazeExit.Location = TileCoordsXYZD(CoordsXYZD(exitOffset, 0, tileElement->GetDirection()));
mazeExit.IsExit = true;
entrance_elements.push_back(mazeExit);
2019-06-30 12:23:06 +02:00
// Save global vars as they are still used by scenery????
2021-10-28 22:11:43 +02:00
int32_t startZ = tds.Origin.z;
TrackDesignPreviewDrawOutlines(tds, this, RideGetTemporaryForPreview(), { 4096, 4096, 0, _currentTrackPieceDirection });
2021-10-28 22:11:43 +02:00
tds.Origin = { startLoc.x, startLoc.y, startZ };
2019-06-30 12:23:06 +02:00
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_CONSTRUCT;
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_ARROW;
gMapSelectFlags &= ~MAP_SELECT_FLAG_GREEN;
2021-10-28 22:11:43 +02:00
space_required_x = ((tds.PreviewMax.x - tds.PreviewMin.x) / 32) + 1;
space_required_y = ((tds.PreviewMax.y - tds.PreviewMin.y) / 32) + 1;
return { true };
2019-06-30 12:23:06 +02:00
}
CoordsXYE TrackDesign::MazeGetFirstElement(const Ride& ride)
{
CoordsXYE tile{};
for (tile.y = 0; tile.y < MAXIMUM_MAP_SIZE_BIG; tile.y += COORDS_XY_STEP)
2019-06-30 12:23:06 +02:00
{
for (tile.x = 0; tile.x < MAXIMUM_MAP_SIZE_BIG; tile.x += COORDS_XY_STEP)
2019-06-30 12:23:06 +02:00
{
tile.element = MapGetFirstElementAt(CoordsXY{ tile.x, tile.y });
2019-06-30 12:23:06 +02:00
do
{
if (tile.element == nullptr)
break;
2021-12-11 00:39:39 +01:00
if (tile.element->GetType() != TileElementType::Track)
2019-06-30 12:23:06 +02:00
continue;
if (tile.element->AsTrack()->GetRideIndex() == ride.id)
{
return tile;
}
} while (!(tile.element++)->IsLastForTile());
}
}
tile.element = nullptr;
return tile;
}
ResultWithMessage TrackDesign::CreateTrackDesignScenery(TrackDesignState& tds)
2019-06-30 12:23:06 +02:00
{
scenery_elements = _trackSavedTileElementsDesc;
// Run an element loop
for (auto& scenery : scenery_elements)
{
switch (scenery.scenery_object.GetType())
2019-06-30 12:23:06 +02:00
{
case ObjectType::Paths:
2019-06-30 12:23:06 +02:00
{
uint8_t slope = (scenery.flags & 0x60) >> 5;
slope -= _saveDirection;
scenery.flags &= 0x9F;
scenery.flags |= ((slope & 3) << 5);
// Direction of connection on path
uint8_t direction = scenery.flags & 0xF;
// Rotate the direction by the track direction
direction = ((direction << 4) >> _saveDirection);
scenery.flags &= 0xF0;
scenery.flags |= (direction & 0xF) | (direction >> 4);
break;
}
case ObjectType::Walls:
2019-06-30 12:23:06 +02:00
{
uint8_t direction = scenery.flags & 3;
direction -= _saveDirection;
scenery.flags &= 0xFC;
scenery.flags |= (direction & 3);
break;
}
default:
{
uint8_t direction = scenery.flags & 3;
uint8_t quadrant = (scenery.flags & 0x0C) >> 2;
direction -= _saveDirection;
quadrant -= _saveDirection;
scenery.flags &= 0xF0;
scenery.flags |= (direction & 3) | ((quadrant & 3) << 2);
break;
}
}
const auto relativeMapPosition = scenery.loc - tds.Origin;
const CoordsXY rotatedRelativeMapPos = relativeMapPosition.Rotate(0 - _saveDirection);
2019-06-30 12:23:06 +02:00
if (rotatedRelativeMapPos.x > 127 * COORDS_XY_STEP || rotatedRelativeMapPos.y > 127 * COORDS_XY_STEP
|| rotatedRelativeMapPos.x < -126 * COORDS_XY_STEP || rotatedRelativeMapPos.y < -126 * COORDS_XY_STEP)
2019-06-30 12:23:06 +02:00
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
if (relativeMapPosition.z > 127 * COORDS_Z_STEP || relativeMapPosition.z < -126 * COORDS_Z_STEP)
2019-06-30 12:23:06 +02:00
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
2019-06-30 12:23:06 +02:00
}
scenery.loc = CoordsXYZ(rotatedRelativeMapPos, relativeMapPosition.z);
2019-06-30 12:23:06 +02:00
}
return { true };
2019-06-30 12:23:06 +02:00
}
void TrackDesign::Serialise(DataSerialiser& stream)
{
2019-12-15 10:18:02 +01:00
if (stream.IsLogging())
{
stream << DS_TAG(name);
// There is too much information logged.
// See sub actions for this information if required.
return;
}
stream << DS_TAG(type);
stream << DS_TAG(vehicle_type);
stream << DS_TAG(cost);
stream << DS_TAG(flags);
stream << DS_TAG(ride_mode);
stream << DS_TAG(track_flags);
stream << DS_TAG(colour_scheme);
stream << DS_TAG(vehicle_colours);
stream << DS_TAG(StationObjectIdentifier);
2019-12-15 10:18:02 +01:00
stream << DS_TAG(total_air_time);
stream << DS_TAG(depart_flags);
stream << DS_TAG(number_of_trains);
stream << DS_TAG(number_of_cars_per_train);
stream << DS_TAG(min_waiting_time);
stream << DS_TAG(max_waiting_time);
stream << DS_TAG(operation_setting);
stream << DS_TAG(max_speed);
stream << DS_TAG(average_speed);
stream << DS_TAG(ride_length);
stream << DS_TAG(max_positive_vertical_g);
stream << DS_TAG(max_negative_vertical_g);
stream << DS_TAG(max_lateral_g);
stream << DS_TAG(inversions);
stream << DS_TAG(holes);
stream << DS_TAG(drops);
stream << DS_TAG(highest_drop_height);
stream << DS_TAG(excitement);
stream << DS_TAG(intensity);
stream << DS_TAG(nausea);
stream << DS_TAG(upkeep_cost);
stream << DS_TAG(track_spine_colour);
stream << DS_TAG(track_rail_colour);
stream << DS_TAG(track_support_colour);
stream << DS_TAG(flags2);
stream << DS_TAG(vehicle_object);
2019-12-15 10:18:02 +01:00
stream << DS_TAG(space_required_x);
stream << DS_TAG(space_required_y);
stream << DS_TAG(lift_hill_speed);
stream << DS_TAG(num_circuits);
stream << DS_TAG(maze_elements);
stream << DS_TAG(track_elements);
stream << DS_TAG(entrance_elements);
stream << DS_TAG(scenery_elements);
stream << DS_TAG(name);
}
std::unique_ptr<TrackDesign> TrackDesignImport(const utf8* path)
{
try
{
auto trackImporter = TrackImporter::Create(path);
trackImporter->Load(path);
2019-06-30 12:23:06 +02:00
return trackImporter->Import();
}
catch (const std::exception& e)
{
LOG_ERROR("Unable to load track design: %s", e.what());
}
LOG_VERBOSE("track_design_open(\"%s\")", path);
2017-10-03 00:00:32 +02:00
return nullptr;
}
/**
*
* rct2: 0x006ABDB0
*/
static void TrackDesignLoadSceneryObjects(TrackDesign* td6)
{
auto& objectManager = OpenRCT2::GetContext()->GetObjectManager();
objectManager.UnloadAllTransient();
// Load ride object
if (td6->vehicle_object.HasValue())
{
objectManager.LoadObject(td6->vehicle_object);
}
// Load scenery objects
2019-04-08 20:34:24 +02:00
for (const auto& scenery : td6->scenery_elements)
{
if (scenery.scenery_object.HasValue())
{
objectManager.LoadObject(scenery.scenery_object);
}
}
}
struct TrackSceneryEntry
2016-04-30 20:55:13 +02:00
{
ObjectType Type = ObjectType::None;
ObjectEntryIndex Index = OBJECT_ENTRY_INDEX_NULL;
ObjectEntryIndex SecondaryIndex = OBJECT_ENTRY_INDEX_NULL; // For footpath railing
};
static ObjectEntryIndex TrackDesignGetDefaultSurfaceIndex(bool isQueue)
{
for (ObjectEntryIndex i = 0; i < MAX_FOOTPATH_SURFACE_OBJECTS; i++)
{
auto footpathSurfaceObj = GetPathSurfaceEntry(i);
if (footpathSurfaceObj != nullptr)
{
if (footpathSurfaceObj->Flags & FOOTPATH_ENTRY_FLAG_SHOW_ONLY_IN_SCENARIO_EDITOR)
{
continue;
}
if (isQueue != ((footpathSurfaceObj->Flags & FOOTPATH_ENTRY_FLAG_IS_QUEUE) != 0))
{
continue;
}
return i;
}
}
return OBJECT_ENTRY_INDEX_NULL;
}
static ObjectEntryIndex TrackDesignGetDefaultRailingIndex()
{
for (ObjectEntryIndex i = 0; i < MAX_FOOTPATH_RAILINGS_OBJECTS; i++)
{
auto footpathRailingsObj = GetPathRailingsEntry(i);
if (footpathRailingsObj != nullptr)
{
return i;
}
}
return OBJECT_ENTRY_INDEX_NULL;
}
static std::optional<TrackSceneryEntry> TrackDesignPlaceSceneryElementGetEntry(const TrackDesignSceneryElement& scenery)
{
TrackSceneryEntry result;
auto& objectMgr = OpenRCT2::GetContext()->GetObjectManager();
if (scenery.scenery_object.GetType() == ObjectType::Paths)
{
auto footpathMapping = RCT2::GetFootpathSurfaceId(scenery.scenery_object, true, scenery.IsQueue());
if (footpathMapping == nullptr)
{
// Check if legacy path object is loaded
auto obj = objectMgr.GetLoadedObject(scenery.scenery_object);
if (obj != nullptr)
{
result.Type = obj->GetObjectType();
result.Index = objectMgr.GetLoadedObjectEntryIndex(obj);
}
else
{
result.Type = ObjectType::FootpathSurface;
}
}
else
{
result.Type = ObjectType::FootpathSurface;
result.Index = objectMgr.GetLoadedObjectEntryIndex(
ObjectEntryDescriptor(scenery.IsQueue() ? footpathMapping->QueueSurface : footpathMapping->NormalSurface));
result.SecondaryIndex = objectMgr.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(footpathMapping->Railing));
}
if (result.Index == OBJECT_ENTRY_INDEX_NULL)
result.Index = TrackDesignGetDefaultSurfaceIndex(scenery.IsQueue());
if (result.SecondaryIndex == OBJECT_ENTRY_INDEX_NULL)
result.SecondaryIndex = TrackDesignGetDefaultRailingIndex();
if (result.Index == OBJECT_ENTRY_INDEX_NULL || result.SecondaryIndex == OBJECT_ENTRY_INDEX_NULL)
{
_trackDesignPlaceStateSceneryUnavailable = true;
return {};
}
}
else
{
auto obj = objectMgr.GetLoadedObject(scenery.scenery_object);
bool objectUnavailable = obj == nullptr;
if (obj != nullptr)
{
result.Type = obj->GetObjectType();
result.Index = objectMgr.GetLoadedObjectEntryIndex(obj);
2024-03-03 22:44:15 +01:00
if (!GetGameState().Cheats.IgnoreResearchStatus)
{
objectUnavailable = !ResearchIsInvented(result.Type, result.Index);
}
}
if (objectUnavailable)
{
_trackDesignPlaceStateSceneryUnavailable = true;
return {};
}
}
return result;
}
/**
*
* rct2: 0x006D247A
*/
static void TrackDesignMirrorScenery(TrackDesign* td6)
{
auto& objectMgr = OpenRCT2::GetContext()->GetObjectManager();
for (auto& scenery : td6->scenery_elements)
{
auto entryInfo = TrackDesignPlaceSceneryElementGetEntry(scenery);
if (!entryInfo)
continue;
auto obj = objectMgr.GetLoadedObject(entryInfo->Type, entryInfo->Index);
switch (obj->GetObjectType())
{
case ObjectType::LargeScenery:
{
auto* sceneryEntry = reinterpret_cast<const LargeSceneryEntry*>(obj->GetLegacyData());
2018-06-22 23:14:18 +02:00
int16_t x1 = 0, x2 = 0, y1 = 0, y2 = 0;
for (LargeSceneryTile* tile = sceneryEntry->tiles; tile->x_offset != -1; tile++)
{
if (x1 > tile->x_offset)
{
x1 = tile->x_offset;
}
if (x2 < tile->x_offset)
{
x2 = tile->x_offset;
}
if (y1 > tile->y_offset)
{
y1 = tile->y_offset;
}
2019-07-31 20:58:21 +02:00
if (y2 < tile->y_offset)
{
y2 = tile->y_offset;
}
}
2019-04-08 20:34:24 +02:00
switch (scenery.flags & 3)
{
2018-06-22 23:14:18 +02:00
case 0:
scenery.loc.y = -(scenery.loc.y + y1) - y2;
2018-06-22 23:14:18 +02:00
break;
case 1:
scenery.loc.x = scenery.loc.x + y2 + y1;
scenery.loc.y = -scenery.loc.y;
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 1);
break;
case 2:
scenery.loc.y = -(scenery.loc.y - y2) + y1;
break;
case 3:
scenery.loc.x = scenery.loc.x - y2 - y1;
scenery.loc.y = -scenery.loc.y;
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 1);
2018-06-22 23:14:18 +02:00
break;
}
break;
}
case ObjectType::SmallScenery:
{
auto* sceneryEntry = reinterpret_cast<const SmallSceneryEntry*>(obj->GetLegacyData());
scenery.loc.y = -scenery.loc.y;
if (sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_DIAGONAL))
{
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 0);
if (!sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE))
2018-06-22 23:14:18 +02:00
{
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 2);
2018-06-22 23:14:18 +02:00
}
break;
}
2019-04-08 20:34:24 +02:00
if (scenery.flags & (1 << 0))
2018-06-22 23:14:18 +02:00
{
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 1);
}
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 2);
2018-06-22 23:14:18 +02:00
break;
}
case ObjectType::Walls:
{
scenery.loc.y = -scenery.loc.y;
2019-04-08 20:34:24 +02:00
if (scenery.flags & (1 << 0))
2018-06-22 23:14:18 +02:00
{
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 1);
2018-06-22 23:14:18 +02:00
}
break;
}
case ObjectType::Paths:
case ObjectType::FootpathSurface:
{
scenery.loc.y = -scenery.loc.y;
2019-04-08 20:34:24 +02:00
if (scenery.flags & (1 << 5))
2018-06-22 23:14:18 +02:00
{
2019-04-08 20:34:24 +02:00
scenery.flags ^= (1 << 6);
2018-06-22 23:14:18 +02:00
}
2019-04-08 20:34:24 +02:00
uint8_t flags = scenery.flags;
2018-06-22 23:14:18 +02:00
flags = ((flags & (1 << 3)) >> 2) | ((flags & (1 << 1)) << 2);
2019-04-08 20:34:24 +02:00
scenery.flags &= 0xF5;
scenery.flags |= flags;
break;
}
default:
break;
}
}
}
static void TrackDesignMirrorEntrances(TrackDesign& td)
{
for (auto& entrance : td.entrance_elements)
{
entrance.Location.y = -entrance.Location.y;
if (entrance.Location.direction & 1)
{
entrance.Location.direction = DirectionReverse(entrance.Location.direction);
}
}
}
/**
*
* rct2: 0x006D2443
*/
static void TrackDesignMirrorRide(TrackDesign* td6)
2016-04-30 20:55:13 +02:00
{
for (auto& track : td6->track_elements)
{
const auto& ted = GetTrackElementDescriptor(track.Type);
track.Type = ted.MirrorElement;
}
}
2016-08-06 16:05:37 +02:00
/** rct2: 0x00993EDC */
static constexpr uint8_t maze_segment_mirror_map[] = {
5, 4, 2, 7, 1, 0, 14, 3, 13, 12, 10, 15, 9, 8, 6, 11,
};
2016-08-06 16:05:37 +02:00
/**
*
* rct2: 0x006D25FA
*/
static void TrackDesignMirrorMaze(TrackDesign* td6)
2016-04-30 20:55:13 +02:00
{
for (auto& maze : td6->maze_elements)
{
2019-04-08 20:34:24 +02:00
maze.y = -maze.y;
2019-04-08 20:34:24 +02:00
uint16_t maze_entry = maze.maze_entry;
2018-06-22 23:14:18 +02:00
uint16_t new_entry = 0;
for (uint8_t position = UtilBitScanForward(maze_entry); position != 0xFF; position = UtilBitScanForward(maze_entry))
{
maze_entry &= ~(1 << position);
new_entry |= (1 << maze_segment_mirror_map[position]);
}
2019-04-08 20:34:24 +02:00
maze.maze_entry = new_entry;
}
}
/**
*
* rct2: 0x006D2436
*/
void TrackDesignMirror(TrackDesign* td6)
{
2022-12-14 14:21:21 +01:00
const auto& rtd = GetRideTypeDescriptor(td6->type);
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
{
TrackDesignMirrorMaze(td6);
}
else
{
TrackDesignMirrorRide(td6);
}
TrackDesignMirrorEntrances(*td6);
TrackDesignMirrorScenery(td6);
}
static void TrackDesignAddSelectedTile(const CoordsXY& coords)
{
auto tileIterator = std::find(gMapSelectionTiles.begin(), gMapSelectionTiles.end(), coords);
if (tileIterator == gMapSelectionTiles.end())
{
gMapSelectionTiles.push_back(coords);
}
}
static void TrackDesignUpdatePreviewBounds(TrackDesignState& tds, const CoordsXYZ& coords)
2016-04-30 23:29:13 +02:00
{
2021-10-28 22:11:43 +02:00
tds.PreviewMin = { std::min(tds.PreviewMin.x, coords.x), std::min(tds.PreviewMin.y, coords.y),
std::min(tds.PreviewMin.z, coords.z) };
tds.PreviewMax = { std::max(tds.PreviewMax.x, coords.x), std::max(tds.PreviewMax.y, coords.y),
std::max(tds.PreviewMax.z, coords.z) };
2016-04-30 23:29:13 +02:00
}
static GameActions::Result TrackDesignPlaceSceneryElementRemoveGhost(
CoordsXY mapCoord, const TrackDesignSceneryElement& scenery, uint8_t rotation, int32_t originZ)
{
auto entryInfo = TrackDesignPlaceSceneryElementGetEntry(scenery);
if (!entryInfo)
{
return GameActions::Result();
2019-03-28 19:02:02 +01:00
}
2019-03-28 19:02:02 +01:00
if (_trackDesignPlaceStateSceneryUnavailable)
{
return GameActions::Result();
}
2019-03-28 19:02:02 +01:00
int32_t z = scenery.loc.z + originZ;
uint8_t sceneryRotation = (rotation + scenery.flags) & kTileElementDirectionMask;
2019-04-01 19:58:16 +02:00
const uint32_t flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND
| GAME_COMMAND_FLAG_GHOST;
std::unique_ptr<GameAction> ga;
switch (entryInfo->Type)
{
case ObjectType::SmallScenery:
{
2019-04-08 20:34:24 +02:00
uint8_t quadrant = (scenery.flags >> 2) + _currentTrackPieceDirection;
quadrant &= 3;
auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry<SmallSceneryEntry>(entryInfo->Index);
if (!(!sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE) && sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_DIAGONAL))
&& sceneryEntry->HasFlag(
2019-11-04 14:02:30 +01:00
SMALL_SCENERY_FLAG_DIAGONAL | SMALL_SCENERY_FLAG_HALF_SPACE | SMALL_SCENERY_FLAG_THREE_QUARTERS))
{
quadrant = 0;
}
ga = std::make_unique<SmallSceneryRemoveAction>(CoordsXYZ{ mapCoord.x, mapCoord.y, z }, quadrant, entryInfo->Index);
break;
}
case ObjectType::LargeScenery:
2020-03-07 21:07:18 +01:00
ga = std::make_unique<LargeSceneryRemoveAction>(CoordsXYZD{ mapCoord.x, mapCoord.y, z, sceneryRotation }, 0);
break;
case ObjectType::Walls:
2020-03-07 21:07:18 +01:00
ga = std::make_unique<WallRemoveAction>(CoordsXYZD{ mapCoord.x, mapCoord.y, z, sceneryRotation });
break;
case ObjectType::Paths:
case ObjectType::FootpathSurface:
2020-03-07 21:07:18 +01:00
ga = std::make_unique<FootpathRemoveAction>(CoordsXYZ{ mapCoord.x, mapCoord.y, z });
break;
default:
return GameActions::Result();
}
ga->SetFlags(flags);
return GameActions::ExecuteNested(ga.get());
}
static bool TrackDesignPlaceSceneryElementGetPlaceZ(TrackDesignState& tds, const TrackDesignSceneryElement& scenery)
{
int32_t z = scenery.loc.z + tds.PlaceZ;
2021-10-28 22:11:43 +02:00
if (z < tds.PlaceSceneryZ)
{
2021-10-28 22:11:43 +02:00
tds.PlaceSceneryZ = z;
}
TrackDesignPlaceSceneryElementGetEntry(scenery);
return true;
}
static GameActions::Result TrackDesignPlaceSceneryElement(
TrackDesignState& tds, CoordsXY mapCoord, uint8_t mode, const TrackDesignSceneryElement& scenery, uint8_t rotation,
int32_t originZ)
{
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_DRAW_OUTLINES && mode == 0)
{
TrackDesignAddSelectedTile(mapCoord);
return GameActions::Result();
}
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_REMOVE_GHOST && mode == 0)
{
return TrackDesignPlaceSceneryElementRemoveGhost(mapCoord, scenery, rotation, originZ);
}
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_GET_PLACE_Z)
{
TrackDesignPlaceSceneryElementGetPlaceZ(tds, scenery);
return GameActions::Result();
}
money64 cost = 0;
2021-08-08 12:27:06 +02:00
if (tds.PlaceOperation != PTD_OPERATION_PLACE_QUERY && tds.PlaceOperation != PTD_OPERATION_PLACE
&& tds.PlaceOperation != PTD_OPERATION_PLACE_GHOST && tds.PlaceOperation != PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
return GameActions::Result();
}
auto entryInfo = TrackDesignPlaceSceneryElementGetEntry(scenery);
if (!entryInfo)
{
return GameActions::Result();
}
int16_t z;
uint8_t flags;
uint8_t quadrant;
switch (entryInfo->Type)
{
case ObjectType::SmallScenery:
{
if (mode != 0)
{
return GameActions::Result();
}
rotation += scenery.flags;
rotation &= 3;
z = scenery.loc.z + originZ;
quadrant = ((scenery.flags >> 2) + _currentTrackPieceDirection) & 3;
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_TRACK_DESIGN;
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_TRACK_DESIGN | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED
| GAME_COMMAND_FLAG_NO_SPEND;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_TRACK_DESIGN | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED
| GAME_COMMAND_FLAG_GHOST | GAME_COMMAND_FLAG_NO_SPEND;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
flags = GAME_COMMAND_FLAG_TRACK_DESIGN;
}
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
auto smallSceneryPlace = SmallSceneryPlaceAction(
{ mapCoord.x, mapCoord.y, z, rotation }, quadrant, entryInfo->Index, scenery.primary_colour,
scenery.secondary_colour, COLOUR_DARK_BROWN);
smallSceneryPlace.SetFlags(flags);
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&smallSceneryPlace)
: GameActions::QueryNested(&smallSceneryPlace);
cost = res.Error == GameActions::Status::Ok ? res.Cost : 0;
break;
}
case ObjectType::LargeScenery:
{
if (mode != 0)
{
return GameActions::Result();
}
rotation += scenery.flags;
rotation &= 3;
z = scenery.loc.z + originZ;
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_TRACK_DESIGN;
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_TRACK_DESIGN | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED
| GAME_COMMAND_FLAG_NO_SPEND;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_TRACK_DESIGN | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED
| GAME_COMMAND_FLAG_GHOST | GAME_COMMAND_FLAG_NO_SPEND;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
flags = GAME_COMMAND_FLAG_TRACK_DESIGN;
}
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
auto sceneryPlaceAction = LargeSceneryPlaceAction(
{ mapCoord.x, mapCoord.y, z, rotation }, entryInfo->Index, scenery.primary_colour, scenery.secondary_colour,
COLOUR_DARK_BROWN);
sceneryPlaceAction.SetFlags(flags);
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&sceneryPlaceAction)
: GameActions::QueryNested(&sceneryPlaceAction);
2018-12-22 19:46:59 +01:00
cost = res.Cost;
break;
}
case ObjectType::Walls:
{
if (mode != 0)
{
return GameActions::Result();
}
2018-12-22 19:46:59 +01:00
z = scenery.loc.z + originZ;
rotation += scenery.flags;
rotation &= 3;
flags = GAME_COMMAND_FLAG_APPLY;
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_TRACK_DESIGN | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED
| GAME_COMMAND_FLAG_NO_SPEND;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
2019-04-06 09:01:22 +02:00
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND
| GAME_COMMAND_FLAG_GHOST;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
flags = 0;
}
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
auto wallPlaceAction = WallPlaceAction(
entryInfo->Index, { mapCoord.x, mapCoord.y, z }, rotation, scenery.primary_colour, scenery.secondary_colour,
(scenery.flags & 0xFC) >> 2);
wallPlaceAction.SetFlags(flags);
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&wallPlaceAction)
: GameActions::QueryNested(&wallPlaceAction);
cost = res.Cost;
break;
}
case ObjectType::Paths:
case ObjectType::FootpathSurface:
z = scenery.loc.z + originZ;
if (mode == 0)
{
auto isQueue = scenery.IsQueue();
uint8_t bh = ((scenery.flags & 0xF) << rotation);
flags = bh >> 4;
bh = (bh | flags) & 0xF;
flags = (((scenery.flags >> 5) + rotation) & 3) << 5;
bh |= flags;
bh |= scenery.flags & 0x90;
2018-12-22 19:46:59 +01:00
flags = GAME_COMMAND_FLAG_APPLY;
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND;
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND
| GAME_COMMAND_FLAG_GHOST;
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
flags = 0;
}
2021-10-28 22:11:43 +02:00
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
uint8_t slope = ((bh >> 5) & 0x3) | ((bh >> 2) & 0x4);
uint8_t edges = bh & 0xF;
PathConstructFlags constructFlags = 0;
if (isQueue)
constructFlags |= PathConstructFlag::IsQueue;
if (entryInfo->Type == ObjectType::Paths)
constructFlags |= PathConstructFlag::IsLegacyPathObject;
auto footpathPlaceAction = FootpathLayoutPlaceAction(
{ mapCoord.x, mapCoord.y, z }, slope, entryInfo->Index, entryInfo->SecondaryIndex, edges, constructFlags);
footpathPlaceAction.SetFlags(flags);
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&footpathPlaceAction)
: GameActions::QueryNested(&footpathPlaceAction);
// Ignore failures
cost = res.Error == GameActions::Status::Ok ? res.Cost : 0;
2019-04-06 09:01:22 +02:00
}
else
{
if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
return GameActions::Result();
}
auto* pathElement = MapGetPathElementAt(TileCoordsXYZ{ CoordsXYZ{ mapCoord.x, mapCoord.y, z } });
if (pathElement == nullptr)
{
return GameActions::Result();
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE)
{
2022-10-04 08:51:27 +02:00
FootpathQueueChainReset();
FootpathRemoveEdgesAt(mapCoord, reinterpret_cast<TileElement*>(pathElement));
}
flags = GAME_COMMAND_FLAG_APPLY;
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND;
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
{
2019-04-01 19:58:16 +02:00
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND
| GAME_COMMAND_FLAG_GHOST;
}
2021-10-28 22:11:43 +02:00
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE)
{
2022-10-04 08:51:27 +02:00
FootpathConnectEdges(mapCoord, reinterpret_cast<TileElement*>(pathElement), flags);
FootpathUpdateQueueChains();
}
return GameActions::Result();
}
break;
default:
_trackDesignPlaceStateSceneryUnavailable = true;
return GameActions::Result();
}
auto res = GameActions::Result();
res.Cost = cost;
return res;
}
/**
*
* rct2: 0x006D0964
*/
static GameActions::Result TrackDesignPlaceAllScenery(
TrackDesignState& tds, const std::vector<TrackDesignSceneryElement>& sceneryList, uint8_t rotation)
{
2021-10-28 22:11:43 +02:00
const auto& origin = tds.Origin;
money64 cost = 0;
for (uint8_t mode = 0; mode <= 1; mode++)
{
2019-04-08 20:34:24 +02:00
if (!sceneryList.empty())
{
2021-10-28 22:11:43 +02:00
tds.HasScenery = true;
}
2021-10-28 22:11:43 +02:00
if (!tds.PlaceScenery)
{
continue;
}
2019-04-08 20:34:24 +02:00
for (const auto& scenery : sceneryList)
{
auto mapCoord = CoordsXYZ{ CoordsXY(origin) + scenery.loc.Rotate(rotation), origin.z };
TrackDesignUpdatePreviewBounds(tds, mapCoord);
auto placementRes = TrackDesignPlaceSceneryElement(tds, mapCoord, mode, scenery, rotation, origin.z);
if (placementRes.Error != GameActions::Status::Ok)
{
2021-10-29 19:11:03 +02:00
// Allow operation to fail when its removing ghosts.
if (tds.PlaceOperation != PTD_OPERATION_REMOVE_GHOST)
{
return placementRes;
}
}
cost += placementRes.Cost;
}
}
auto res = GameActions::Result();
res.Cost = cost;
return res;
}
static std::optional<GameActions::Result> TrackDesignPlaceEntrances(
TrackDesignState& tds, const TrackDesign& td, CoordsXYZ newCoords, RideId rideId, money64& totalCost)
{
for (const auto& entrance : td.entrance_elements)
{
auto rotation = _currentTrackPieceDirection & 3;
CoordsXY entranceMapPos = entrance.Location.ToCoordsXY();
auto rotatedEntranceMapPos = entranceMapPos.Rotate(rotation);
newCoords = { rotatedEntranceMapPos + tds.Origin, newCoords.z };
TrackDesignUpdatePreviewBounds(tds, newCoords);
switch (tds.PlaceOperation)
{
case PTD_OPERATION_DRAW_OUTLINES:
TrackDesignAddSelectedTile(newCoords);
break;
case PTD_OPERATION_PLACE_QUERY:
case PTD_OPERATION_PLACE:
case PTD_OPERATION_PLACE_GHOST:
case PTD_OPERATION_PLACE_TRACK_PREVIEW:
{
rotation = (rotation + entrance.Location.direction) & 3;
newCoords.z = entrance.Location.z * COORDS_Z_STEP;
newCoords.z += tds.Origin.z;
if (tds.PlaceOperation != PTD_OPERATION_PLACE_QUERY)
{
auto tile = CoordsXY{ newCoords } + CoordsDirectionDelta[rotation];
TileElement* tile_element = MapGetFirstElementAt(tile);
if (tile_element == nullptr)
{
2024-01-22 11:29:08 +01:00
return GameActions::Result(
GameActions::Status::InvalidParameters, STR_ERR_INVALID_PARAMETER, STR_ERR_TILE_ELEMENT_NOT_FOUND);
}
do
2018-06-22 23:14:18 +02:00
{
if (tile_element->GetType() != TileElementType::Track)
{
continue;
}
if (tile_element->GetBaseZ() != newCoords.z)
{
continue;
}
2018-06-22 23:14:18 +02:00
auto stationIndex = tile_element->AsTrack()->GetStationIndex();
uint8_t flags = GAME_COMMAND_FLAG_APPLY;
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
2018-06-22 23:14:18 +02:00
{
2019-04-02 19:17:37 +02:00
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED
| GAME_COMMAND_FLAG_NO_SPEND;
2018-06-22 23:14:18 +02:00
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
2018-06-22 23:14:18 +02:00
{
2019-04-01 19:58:16 +02:00
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND
2018-06-22 23:14:18 +02:00
| GAME_COMMAND_FLAG_GHOST;
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
flags = 0;
}
2021-10-28 22:11:43 +02:00
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
auto rideEntranceExitPlaceAction = RideEntranceExitPlaceAction(
newCoords, rotation, rideId, stationIndex, entrance.IsExit);
rideEntranceExitPlaceAction.SetFlags(flags);
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&rideEntranceExitPlaceAction)
: GameActions::QueryNested(&rideEntranceExitPlaceAction);
if (res.Error != GameActions::Status::Ok)
{
return res;
}
totalCost += res.Cost;
tds.EntranceExitPlaced = true;
_trackDesignPlaceStateEntranceExitPlaced = true;
break;
} while (!(tile_element++)->IsLastForTile());
}
else
{
auto res = RideEntranceExitPlaceAction::TrackPlaceQuery(newCoords, false);
if (res.Error != GameActions::Status::Ok)
{
return res;
2018-06-22 23:14:18 +02:00
}
totalCost += res.Cost;
tds.EntranceExitPlaced = true;
_trackDesignPlaceStateEntranceExitPlaced = true;
}
break;
}
}
}
return std::nullopt;
}
static GameActions::Result TrackDesignPlaceMaze(
TrackDesignState& tds, TrackDesign& td, const CoordsXYZ& origin, const Ride& ride)
{
if (tds.PlaceOperation == PTD_OPERATION_DRAW_OUTLINES)
{
gMapSelectionTiles.clear();
gMapSelectArrowPosition = CoordsXYZ{ origin, TileElementHeight(origin) };
gMapSelectArrowDirection = _currentTrackPieceDirection;
}
tds.PlaceZ = 0;
money64 totalCost = 0;
for (const auto& maze_element : td.maze_elements)
{
uint8_t rotation = _currentTrackPieceDirection & 3;
CoordsXY mazeMapPos = TileCoordsXY(maze_element.x, maze_element.y).ToCoordsXY();
auto mapCoord = mazeMapPos.Rotate(rotation);
mapCoord += origin;
TrackDesignUpdatePreviewBounds(tds, { mapCoord, origin.z });
if (tds.PlaceOperation == PTD_OPERATION_DRAW_OUTLINES)
{
TrackDesignAddSelectedTile(mapCoord);
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY || tds.PlaceOperation == PTD_OPERATION_PLACE
|| tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST || tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
uint8_t flags;
money64 cost = 0;
uint16_t maze_entry = Numerics::rol16(maze_element.maze_entry, rotation * 4);
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
{
flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND
| GAME_COMMAND_FLAG_GHOST;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
flags = 0;
}
else
{
flags = GAME_COMMAND_FLAG_APPLY;
}
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
auto mazePlace = MazePlaceTrackAction({ mapCoord, origin.z }, ride.id, maze_entry);
mazePlace.SetFlags(flags);
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&mazePlace)
: GameActions::QueryNested(&mazePlace);
if (res.Error != GameActions::Status::Ok)
{
return res;
}
cost = res.Cost;
2021-07-31 22:18:15 +02:00
totalCost += cost;
}
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_GET_PLACE_Z)
{
if (!MapIsLocationValid(mapCoord))
{
continue;
}
auto surfaceElement = MapGetSurfaceElementAt(mapCoord);
if (surfaceElement == nullptr)
continue;
int16_t surfaceZ = surfaceElement->GetBaseZ();
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP)
{
2020-03-07 21:07:18 +01:00
surfaceZ += LAND_HEIGHT_STEP;
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
{
2020-03-07 21:07:18 +01:00
surfaceZ += LAND_HEIGHT_STEP;
}
}
int16_t waterZ = surfaceElement->GetWaterHeight();
if (waterZ > 0 && waterZ > surfaceZ)
{
surfaceZ = waterZ;
}
int16_t temp_z = origin.z + tds.PlaceZ - surfaceZ;
if (temp_z < 0)
{
2021-10-28 22:11:43 +02:00
tds.PlaceZ -= temp_z;
}
}
}
tds.Origin = origin;
auto result = TrackDesignPlaceEntrances(tds, td, origin, ride.id, totalCost);
if (result.has_value())
{
return result.value();
}
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_REMOVE_GHOST)
{
auto gameAction = RideDemolishAction(ride.id, RIDE_MODIFY_DEMOLISH);
2022-08-11 00:00:58 +02:00
gameAction.SetFlags(GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND | GAME_COMMAND_FLAG_GHOST);
GameActions::Execute(&gameAction);
}
auto res = GameActions::Result();
res.Cost = totalCost;
return res;
}
static GameActions::Result TrackDesignPlaceRide(TrackDesignState& tds, TrackDesign* td6, const CoordsXYZ& origin, Ride& ride)
{
2021-10-28 22:11:43 +02:00
tds.Origin = origin;
if (tds.PlaceOperation == PTD_OPERATION_DRAW_OUTLINES)
{
2019-03-28 19:29:51 +01:00
gMapSelectionTiles.clear();
gMapSelectArrowPosition = CoordsXYZ{ origin, TileElementHeight(origin) };
gMapSelectArrowDirection = _currentTrackPieceDirection;
}
2021-10-28 22:11:43 +02:00
tds.PlaceZ = 0;
money64 totalCost = 0;
uint8_t rotation = _currentTrackPieceDirection;
// Track elements
auto newCoords = origin;
2019-04-08 20:34:24 +02:00
for (const auto& track : td6->track_elements)
{
auto trackType = track.Type;
2021-08-27 23:49:32 +02:00
const auto& ted = GetTrackElementDescriptor(trackType);
TrackDesignUpdatePreviewBounds(tds, newCoords);
2021-10-28 22:11:43 +02:00
switch (tds.PlaceOperation)
{
2018-06-22 23:14:18 +02:00
case PTD_OPERATION_DRAW_OUTLINES:
for (const PreviewTrack* trackBlock = ted.Block; trackBlock->index != 0xFF; trackBlock++)
2018-06-22 23:14:18 +02:00
{
auto tile = CoordsXY{ newCoords } + CoordsXY{ trackBlock->x, trackBlock->y }.Rotate(rotation);
TrackDesignUpdatePreviewBounds(tds, { tile, newCoords.z });
TrackDesignAddSelectedTile(tile);
2018-06-22 23:14:18 +02:00
}
break;
2019-03-27 20:33:27 +01:00
case PTD_OPERATION_REMOVE_GHOST:
2017-07-24 18:43:57 +02:00
{
const TrackCoordinates* trackCoordinates = &ted.Coordinates;
const PreviewTrack* trackBlock = ted.Block;
int32_t tempZ = newCoords.z - trackCoordinates->z_begin + trackBlock->z;
auto trackRemoveAction = TrackRemoveAction(
trackType, 0, { newCoords, tempZ, static_cast<Direction>(rotation & 3) });
2019-02-20 11:49:25 +01:00
trackRemoveAction.SetFlags(
GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND | GAME_COMMAND_FLAG_GHOST
| GAME_COMMAND_FLAG_TRACK_DESIGN);
2019-02-20 11:49:25 +01:00
GameActions::ExecuteNested(&trackRemoveAction);
2018-06-22 23:14:18 +02:00
break;
2017-07-24 18:43:57 +02:00
}
2019-03-27 20:21:13 +01:00
case PTD_OPERATION_PLACE_QUERY:
case PTD_OPERATION_PLACE:
case PTD_OPERATION_PLACE_GHOST:
case PTD_OPERATION_PLACE_TRACK_PREVIEW:
2017-07-24 18:43:57 +02:00
{
const TrackCoordinates* trackCoordinates = &ted.Coordinates;
2018-06-22 23:14:18 +02:00
// di
int16_t tempZ = newCoords.z - trackCoordinates->z_begin;
int32_t liftHillAndAlternativeState = 0;
if (track.HasFlag(TrackDesignTrackElementFlag::HasChain))
{
liftHillAndAlternativeState |= 1;
}
if (track.HasFlag(TrackDesignTrackElementFlag::IsInverted))
{
liftHillAndAlternativeState |= 2;
}
2018-06-22 23:14:18 +02:00
uint8_t flags = GAME_COMMAND_FLAG_APPLY;
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
2018-06-22 23:14:18 +02:00
flags |= GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED;
2019-04-01 19:58:16 +02:00
flags |= GAME_COMMAND_FLAG_NO_SPEND;
2018-06-22 23:14:18 +02:00
}
2021-10-28 22:11:43 +02:00
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
2018-06-22 23:14:18 +02:00
{
flags |= GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED;
2019-04-01 19:58:16 +02:00
flags |= GAME_COMMAND_FLAG_NO_SPEND;
2018-06-22 23:14:18 +02:00
flags |= GAME_COMMAND_FLAG_GHOST;
}
2021-10-28 22:11:43 +02:00
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
2018-06-22 23:14:18 +02:00
{
flags = GAME_COMMAND_FLAG_NO_SPEND;
}
2021-10-28 22:11:43 +02:00
if (tds.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
auto trackPlaceAction = TrackPlaceAction(
ride.id, trackType, ride.type, { newCoords, tempZ, static_cast<uint8_t>(rotation) },
track.BrakeBoosterSpeed, track.ColourScheme, track.SeatRotation, liftHillAndAlternativeState, true);
trackPlaceAction.SetFlags(flags);
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&trackPlaceAction)
: GameActions::QueryNested(&trackPlaceAction);
if (res.Error != GameActions::Status::Ok)
{
return res;
}
totalCost += res.Cost;
2018-06-22 23:14:18 +02:00
break;
}
case PTD_OPERATION_GET_PLACE_Z:
{
2021-08-27 23:49:32 +02:00
int32_t tempZ = newCoords.z - ted.Coordinates.z_begin;
for (const PreviewTrack* trackBlock = ted.Block; trackBlock->index != 0xFF; trackBlock++)
{
auto tile = CoordsXY{ newCoords } + CoordsXY{ trackBlock->x, trackBlock->y }.Rotate(rotation);
if (!MapIsLocationValid(tile))
2018-06-22 23:14:18 +02:00
{
continue;
}
auto surfaceElement = MapGetSurfaceElementAt(tile);
if (surfaceElement == nullptr)
2018-06-22 23:14:18 +02:00
{
2024-01-22 11:29:08 +01:00
return GameActions::Result(
GameActions::Status::InvalidParameters, STR_ERR_INVALID_PARAMETER,
STR_ERR_SURFACE_ELEMENT_NOT_FOUND);
2018-06-22 23:14:18 +02:00
}
int32_t surfaceZ = surfaceElement->GetBaseZ();
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP)
2018-06-22 23:14:18 +02:00
{
2020-03-07 21:07:18 +01:00
surfaceZ += LAND_HEIGHT_STEP;
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
2018-06-22 23:14:18 +02:00
{
2020-03-07 21:07:18 +01:00
surfaceZ += LAND_HEIGHT_STEP;
2018-06-22 23:14:18 +02:00
}
}
2020-03-31 20:56:47 +02:00
auto waterZ = surfaceElement->GetWaterHeight();
if (waterZ > 0 && waterZ > surfaceZ)
2018-06-22 23:14:18 +02:00
{
surfaceZ = waterZ;
2018-06-22 23:14:18 +02:00
}
2021-10-28 22:11:43 +02:00
int32_t heightDifference = tempZ + tds.PlaceZ + trackBlock->z - surfaceZ;
2018-06-22 23:14:18 +02:00
if (heightDifference < 0)
{
2021-10-28 22:11:43 +02:00
tds.PlaceZ -= heightDifference;
2018-06-22 23:14:18 +02:00
}
}
2018-06-22 23:14:18 +02:00
break;
}
}
const TrackCoordinates& track_coordinates = ted.Coordinates;
auto offsetAndRotatedTrack = CoordsXY{ newCoords }
2021-08-27 23:44:33 +02:00
+ CoordsXY{ track_coordinates.x, track_coordinates.y }.Rotate(rotation);
2021-08-27 23:44:33 +02:00
newCoords = { offsetAndRotatedTrack, newCoords.z - track_coordinates.z_begin + track_coordinates.z_end };
rotation = (rotation + track_coordinates.rotation_end - track_coordinates.rotation_begin) & 3;
if (track_coordinates.rotation_end & (1 << 2))
{
rotation |= (1 << 2);
}
else
{
newCoords += CoordsDirectionDelta[rotation];
}
}
auto result = TrackDesignPlaceEntrances(tds, *td6, newCoords, ride.id, totalCost);
if (result.has_value())
{
return result.value();
}
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_REMOVE_GHOST)
2017-07-24 18:43:57 +02:00
{
ride.ValidateStations();
ride.Delete();
}
auto res = GameActions::Result();
res.Cost = totalCost;
return res;
}
/**
* Places a virtual track. This can involve highlighting the surface tiles and showing the track layout. It is also used by
* the track preview window to place the whole track.
* Depending on the value of bl it modifies the function.
* bl == 0, Draw outlines on the ground
* bl == 1,
* bl == 2,
* bl == 3, Returns the z value of a successful placement. Only lower 16 bits are the value, the rest may be garbage?
* bl == 4,
* bl == 5, Returns cost to create the track. All 32 bits are used. Places the track. (used by the preview)
* bl == 6, Clear white outlined track.
* rct2: 0x006D01B3
*/
static GameActions::Result TrackDesignPlaceVirtual(
TrackDesignState& tds, TrackDesign* td6, uint8_t ptdOperation, bool placeScenery, Ride& ride, const CoordsXYZD& coords)
{
_trackDesignPlaceStateSceneryUnavailable = false;
_trackDesignPlaceStateEntranceExitPlaced = false;
2021-10-28 22:11:43 +02:00
tds.PlaceScenery = placeScenery;
tds.EntranceExitPlaced = false;
tds.HasScenery = false;
2021-10-28 21:43:33 +02:00
2021-10-28 22:11:43 +02:00
tds.IsReplay = ptdOperation & PTD_OPERATION_FLAG_IS_REPLAY;
ptdOperation &= ~PTD_OPERATION_FLAG_IS_REPLAY;
2021-10-28 22:11:43 +02:00
tds.PlaceOperation = ptdOperation;
2021-10-28 22:11:43 +02:00
tds.PreviewMin = coords;
tds.PreviewMax = coords;
tds.PlaceSceneryZ = 0;
2021-10-28 21:43:33 +02:00
if (gTrackDesignSceneryToggle)
{
2021-10-28 22:11:43 +02:00
tds.PlaceScenery = false;
}
// NOTE: We need to save this, in networked games this would affect all clients otherwise.
auto savedRideId = _currentRideIndex;
auto savedTrackPieceDirection = _currentTrackPieceDirection;
_currentRideIndex = ride.id;
_currentTrackPieceDirection = coords.direction;
GameActions::Result trackPlaceRes;
2022-12-14 14:21:21 +01:00
const auto& rtd = GetRideTypeDescriptor(td6->type);
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
{
trackPlaceRes = TrackDesignPlaceMaze(tds, *td6, coords, ride);
}
else
{
trackPlaceRes = TrackDesignPlaceRide(tds, td6, coords, ride);
}
_currentRideIndex = savedRideId;
_currentTrackPieceDirection = savedTrackPieceDirection;
if (trackPlaceRes.Error != GameActions::Status::Ok)
{
return trackPlaceRes;
}
2021-08-08 12:27:06 +02:00
// Scenery elements
auto sceneryPlaceRes = TrackDesignPlaceAllScenery(tds, td6->scenery_elements, coords.direction);
if (sceneryPlaceRes.Error != GameActions::Status::Ok)
{
return sceneryPlaceRes;
}
// 0x6D0FE6
2021-10-28 22:11:43 +02:00
if (tds.PlaceOperation == PTD_OPERATION_DRAW_OUTLINES)
{
gMapSelectFlags |= MAP_SELECT_FLAG_ENABLE_CONSTRUCT;
gMapSelectFlags |= MAP_SELECT_FLAG_ENABLE_ARROW;
gMapSelectFlags &= ~MAP_SELECT_FLAG_GREEN;
MapInvalidateMapSelectionTiles();
}
auto res = GameActions::Result();
res.Cost = trackPlaceRes.Cost + sceneryPlaceRes.Cost;
2021-08-08 12:27:06 +02:00
return res;
}
GameActions::Result TrackDesignPlace(TrackDesign* td6, uint32_t flags, bool placeScenery, Ride& ride, const CoordsXYZD& coords)
2021-10-28 21:43:33 +02:00
{
uint32_t ptdOperation = (flags & GAME_COMMAND_FLAG_APPLY) != 0 ? PTD_OPERATION_PLACE : PTD_OPERATION_PLACE_QUERY;
if ((flags & GAME_COMMAND_FLAG_APPLY) != 0 && (flags & GAME_COMMAND_FLAG_GHOST) != 0)
{
ptdOperation = PTD_OPERATION_PLACE_GHOST;
}
if (flags & GAME_COMMAND_FLAG_REPLAY)
ptdOperation |= PTD_OPERATION_FLAG_IS_REPLAY;
2021-10-28 21:43:33 +02:00
TrackDesignState tds{};
return TrackDesignPlaceVirtual(tds, td6, ptdOperation, placeScenery, ride, coords);
2021-10-28 21:43:33 +02:00
}
void TrackDesignPreviewRemoveGhosts(TrackDesign* td6, Ride& ride, const CoordsXYZD& coords)
{
TrackDesignState tds{};
TrackDesignPlaceVirtual(tds, td6, PTD_OPERATION_REMOVE_GHOST, true, ride, coords);
}
void TrackDesignPreviewDrawOutlines(TrackDesignState& tds, TrackDesign* td6, Ride& ride, const CoordsXYZD& coords)
{
TrackDesignPlaceVirtual(tds, td6, PTD_OPERATION_DRAW_OUTLINES, true, ride, coords);
}
static int32_t TrackDesignGetZPlacement(TrackDesignState& tds, TrackDesign* td6, Ride& ride, const CoordsXYZD& coords)
{
TrackDesignPlaceVirtual(tds, td6, PTD_OPERATION_GET_PLACE_Z, true, ride, coords);
// Change from vanilla: originally, _trackDesignPlaceSceneryZ was not subtracted
// from _trackDesignPlaceZ, causing bug #259.
return tds.PlaceZ - tds.PlaceSceneryZ;
}
int32_t TrackDesignGetZPlacement(TrackDesign* td6, Ride& ride, const CoordsXYZD& coords)
{
TrackDesignState tds{};
return TrackDesignGetZPlacement(tds, td6, ride, coords);
}
static money64 TrackDesignCreateRide(int32_t type, int32_t subType, int32_t flags, RideId* outRideIndex)
2019-12-11 21:33:58 +01:00
{
// Don't set colours as will be set correctly later.
2024-03-09 11:27:05 +01:00
auto gameAction = RideCreateAction(type, subType, 0, 0, GetGameState().LastEntranceStyle);
2019-12-11 21:33:58 +01:00
gameAction.SetFlags(flags);
auto res = GameActions::ExecuteNested(&gameAction);
2019-12-11 21:33:58 +01:00
// Callee's of this function expect kMoney64Undefined in case of failure.
if (res.Error != GameActions::Status::Ok)
2019-12-11 21:33:58 +01:00
{
return kMoney64Undefined;
2019-12-11 21:33:58 +01:00
}
2022-01-19 14:17:11 +01:00
*outRideIndex = res.GetData<RideId>();
2019-12-11 21:33:58 +01:00
return res.Cost;
2019-12-11 21:33:58 +01:00
}
/**
*
* rct2: 0x006D2189
* ebx = ride_id
* cost = edi
*/
static bool TrackDesignPlacePreview(TrackDesignState& tds, TrackDesign* td6, money64* cost, Ride** outRide, uint8_t* flags)
{
2019-02-13 21:16:42 +01:00
*outRide = nullptr;
2018-06-22 23:14:18 +02:00
*flags = 0;
2024-03-09 11:27:05 +01:00
auto& gameState = GetGameState();
auto& objManager = GetContext()->GetObjectManager();
auto entry_index = objManager.GetLoadedObjectEntryIndex(td6->vehicle_object);
2022-01-19 14:17:11 +01:00
RideId rideIndex;
2019-04-01 19:58:16 +02:00
uint8_t rideCreateFlags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND;
if (TrackDesignCreateRide(td6->type, entry_index, rideCreateFlags, &rideIndex) == kMoney64Undefined)
{
return false;
}
auto ride = GetRide(rideIndex);
if (ride == nullptr)
return false;
2019-07-21 03:22:03 +02:00
ride->custom_name = {};
ride->entrance_style = objManager.GetLoadedObjectEntryIndex(td6->StationObjectIdentifier);
if (ride->entrance_style == OBJECT_ENTRY_INDEX_NULL)
{
2024-03-09 11:27:05 +01:00
ride->entrance_style = gameState.LastEntranceStyle;
}
for (int32_t i = 0; i < OpenRCT2::Limits::NumColourSchemes; i++)
{
ride->track_colour[i].main = td6->track_spine_colour[i];
ride->track_colour[i].additional = td6->track_rail_colour[i];
ride->track_colour[i].supports = td6->track_support_colour[i];
}
// Flat rides need their vehicle colours loaded for display
// in the preview window
if (!GetRideTypeDescriptor(td6->type).HasFlag(RIDE_TYPE_FLAG_HAS_TRACK))
{
for (size_t i = 0; i < std::size(ride->vehicle_colours); i++)
{
ride->vehicle_colours[i] = td6->vehicle_colours[i];
}
}
2021-08-08 18:10:52 +02:00
_trackDesignDrawingPreview = true;
2018-06-22 23:14:18 +02:00
uint8_t backup_rotation = _currentTrackPieceDirection;
uint32_t backup_park_flags = gameState.Park.Flags;
gameState.Park.Flags &= ~PARK_FLAGS_FORBID_HIGH_CONSTRUCTION;
2024-02-12 22:32:08 +01:00
auto mapSize = TileCoordsXY{ gameState.MapSize.x * 16, gameState.MapSize.y * 16 };
_currentTrackPieceDirection = 0;
int32_t z = TrackDesignGetZPlacement(
tds, td6, RideGetTemporaryForPreview(), { mapSize.x, mapSize.y, 16, _currentTrackPieceDirection });
2021-10-28 22:11:43 +02:00
if (tds.HasScenery)
{
*flags |= TRACK_DESIGN_FLAG_HAS_SCENERY;
}
2021-10-28 22:11:43 +02:00
z += 16 - tds.PlaceSceneryZ;
2017-07-24 18:43:57 +02:00
bool placeScenery = true;
if (_trackDesignPlaceStateSceneryUnavailable)
{
2017-07-24 18:43:57 +02:00
placeScenery = false;
*flags |= TRACK_DESIGN_FLAG_SCENERY_UNAVAILABLE;
}
auto res = TrackDesignPlaceVirtual(
tds, td6, PTD_OPERATION_PLACE_TRACK_PREVIEW, placeScenery, *ride,
{ mapSize.x, mapSize.y, z, _currentTrackPieceDirection });
gameState.Park.Flags = backup_park_flags;
if (res.Error == GameActions::Status::Ok)
{
if (entry_index == OBJECT_ENTRY_INDEX_NULL)
{
*flags |= TRACK_DESIGN_FLAG_VEHICLE_UNAVAILABLE;
}
2024-03-03 22:44:15 +01:00
else if (!RideEntryIsInvented(entry_index) && !GetGameState().Cheats.IgnoreResearchStatus)
{
*flags |= TRACK_DESIGN_FLAG_VEHICLE_UNAVAILABLE;
}
_currentTrackPieceDirection = backup_rotation;
2021-08-08 18:10:52 +02:00
_trackDesignDrawingPreview = false;
*cost = res.Cost;
2019-02-13 21:16:42 +01:00
*outRide = ride;
return true;
}
2021-09-15 22:22:15 +02:00
_currentTrackPieceDirection = backup_rotation;
ride->Delete();
_trackDesignDrawingPreview = false;
return false;
}
2016-04-30 23:29:13 +02:00
#pragma region Track Design Preview
2016-04-30 23:29:13 +02:00
/**
*
* rct2: 0x006D1EF0
*/
void TrackDesignDrawPreview(TrackDesign* td6, uint8_t* pixels)
2016-04-30 23:29:13 +02:00
{
StashMap();
TrackDesignPreviewClearMap();
if (gScreenFlags & SCREEN_FLAGS_TRACK_MANAGER)
{
TrackDesignLoadSceneryObjects(td6);
}
TrackDesignState tds{};
money64 cost;
2019-02-13 21:16:42 +01:00
Ride* ride;
2018-06-22 23:14:18 +02:00
uint8_t flags;
if (!TrackDesignPlacePreview(tds, td6, &cost, &ride, &flags))
{
std::fill_n(pixels, kTrackPreviewImageSize * 4, 0x00);
UnstashMap();
return;
}
2018-06-22 23:14:18 +02:00
td6->cost = cost;
td6->track_flags = flags & 7;
2021-10-28 22:11:43 +02:00
CoordsXYZ centre = { (tds.PreviewMin.x + tds.PreviewMax.x) / 2 + 16, (tds.PreviewMin.y + tds.PreviewMax.y) / 2 + 16,
(tds.PreviewMin.z + tds.PreviewMax.z) / 2 };
2021-10-28 22:11:43 +02:00
int32_t size_x = tds.PreviewMax.x - tds.PreviewMin.x;
int32_t size_y = tds.PreviewMax.y - tds.PreviewMin.y;
int32_t size_z = tds.PreviewMax.z - tds.PreviewMin.z;
// Special case for flat rides - Z-axis info is irrelevant
// and must be zeroed out lest the preview be off-centre
if (!GetRideTypeDescriptor(td6->type).HasFlag(RIDE_TYPE_FLAG_HAS_TRACK))
{
centre.z = 0;
size_z = 0;
}
2021-09-29 20:04:56 +02:00
ZoomLevel zoom_level{ 1 };
if (size_x < size_y)
{
size_x = size_y;
}
if (size_x > 1000 || size_z > 280)
{
2021-09-29 20:04:56 +02:00
zoom_level = ZoomLevel{ 2 };
}
if (size_x > 1600 || size_z > 1000)
{
2021-09-29 20:04:56 +02:00
zoom_level = ZoomLevel{ 3 };
}
size_x = zoom_level.ApplyTo(370);
size_y = zoom_level.ApplyTo(217);
Viewport view;
2018-06-22 23:14:18 +02:00
view.width = 370;
view.height = 217;
view.view_width = size_x;
view.view_height = size_y;
view.pos = { 0, 0 };
2018-06-22 23:14:18 +02:00
view.zoom = zoom_level;
2022-03-06 15:26:36 +01:00
view.flags = VIEWPORT_FLAG_HIDE_BASE | VIEWPORT_FLAG_HIDE_ENTITIES;
DrawPixelInfo dpi;
dpi.zoom_level = zoom_level;
2018-06-22 23:14:18 +02:00
dpi.x = 0;
dpi.y = 0;
dpi.width = 370;
dpi.height = 217;
dpi.pitch = 0;
dpi.bits = pixels;
auto drawingEngine = std::make_unique<X8DrawingEngine>(GetContext()->GetUiContext());
dpi.DrawingEngine = drawingEngine.get();
const ScreenCoordsXY offset = { size_x / 2, size_y / 2 };
for (uint8_t i = 0; i < 4; i++)
{
view.viewPos = Translate3DTo2DWithZ(i, centre) - offset;
view.rotation = i;
ViewportRender(dpi, &view, { {}, ScreenCoordsXY{ size_x, size_y } });
dpi.bits += kTrackPreviewImageSize;
}
ride->Delete();
UnstashMap();
}
/**
* Resets all the map elements to surface tiles for track preview.
* rct2: 0x006D1D9A
*/
static void TrackDesignPreviewClearMap()
{
auto numTiles = kMaximumMapSizeTechnical * kMaximumMapSizeTechnical;
2024-02-12 22:32:08 +01:00
GetGameState().MapSize = TRACK_DESIGN_PREVIEW_MAP_SIZE;
// Reserve ~8 elements per tile
std::vector<TileElement> tileElements;
tileElements.reserve(numTiles * 8);
for (int32_t i = 0; i < numTiles; i++)
2017-10-31 12:57:40 +01:00
{
auto* element = &tileElements.emplace_back();
2021-12-11 00:39:39 +01:00
element->ClearAs(TileElementType::Surface);
element->SetLastForTile(true);
element->AsSurface()->SetSlope(TILE_ELEMENT_SLOPE_FLAT);
element->AsSurface()->SetWaterHeight(0);
element->AsSurface()->SetSurfaceObjectIndex(0);
element->AsSurface()->SetEdgeObjectIndex(0);
element->AsSurface()->SetGrassLength(GRASS_LENGTH_CLEAR_0);
element->AsSurface()->SetOwnership(OWNERSHIP_OWNED);
element->AsSurface()->SetParkFences(0);
}
SetTileElements(std::move(tileElements));
}
bool TrackDesignAreEntranceAndExitPlaced()
{
return _trackDesignPlaceStateEntranceExitPlaced;
}
#pragma endregion