OpenRCT2/src/openrct2/ride/TrackDesign.cpp

2131 lines
72 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 "TrackDesign.h"
#include "../Cheats.h"
#include "../Context.h"
#include "../Game.h"
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../TrackImporter.h"
#include "../actions/FootpathLayoutPlaceAction.h"
#include "../actions/FootpathRemoveAction.h"
#include "../actions/LargeSceneryPlaceAction.h"
#include "../actions/LargeSceneryRemoveAction.h"
#include "../actions/MazePlaceTrackAction.h"
#include "../actions/RideCreateAction.h"
#include "../actions/RideDemolishAction.h"
#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"
#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"
#include "../object/LargeSceneryEntry.h"
#include "../object/ObjectEntryManager.h"
#include "../object/ObjectList.h"
#include "../object/ObjectManager.h"
#include "../object/ObjectRepository.h"
#include "../object/SmallSceneryEntry.h"
#include "../object/StationObject.h"
#include "../rct2/RCT2.h"
#include "../ride/RideConstruction.h"
#include "../util/SawyerCoding.h"
#include "../util/Util.h"
#include "../world/Footpath.h"
#include "../world/Park.h"
#include "../world/Scenery.h"
#include "../world/Surface.h"
#include "../world/Wall.h"
#include "Ride.h"
#include "RideData.h"
#include "Track.h"
#include "TrackData.h"
#include "TrackDesign.h"
#include "TrackDesignRepository.h"
#include "Vehicle.h"
#include <algorithm>
#include <iterator>
#include <memory>
using namespace OpenRCT2;
using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::TrackMetaData;
constexpr TileCoordsXY TRACK_DESIGN_PREVIEW_MAP_SIZE = TileCoordsXY{ 256, 256 };
bool gTrackDesignSceneryToggle;
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)
{
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);
}
ride_mode = ride.mode;
colour_scheme = ride.colour_scheme_type & 3;
for (size_t i = 0; i < std::size(vehicle_colours); i++)
{
vehicle_colours[i] = ride.vehicle_colours[i];
}
for (int32_t i = 0; i < OpenRCT2::Limits::NumColourSchemes; i++)
{
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;
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);
ride_length = ride.GetTotalLength() / 65536;
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;
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);
excitement = ride.ratings.Excitement / 10;
intensity = ride.ratings.Intensity / 10;
nausea = ride.ratings.Nausea / 10;
upkeep_cost = ride.upkeep_cost;
flags = 0;
flags2 = 0;
const auto& rtd = GetRideTypeDescriptor(type);
if (rtd.DesignCreateMode == TrackDesignCreateMode::Maze)
{
return CreateTrackDesignMaze(tds, ride);
}
else
{
return CreateTrackDesignTrack(tds, ride);
}
}
ResultWithMessage TrackDesign::CreateTrackDesignTrack(TrackDesignState& tds, const Ride& ride)
{
CoordsXYE trackElement;
if (!RideTryGetOriginElement(ride, &trackElement))
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
StringId warningMessage = STR_NONE;
RideGetStartOfTrack(&trackElement);
int32_t z = trackElement.element->GetBaseZ();
auto trackType = trackElement.element->AsTrack()->GetTrackType();
uint8_t direction = trackElement.element->GetDirection();
_saveDirection = direction;
auto newCoords = GetTrackElementOriginAndApplyChanges(
{ trackElement, z, direction }, trackType, 0, &trackElement.element, 0);
if (!newCoords.has_value())
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
trackElement.x = newCoords->x;
trackElement.y = newCoords->y;
z = newCoords->z;
const auto& ted = GetTrackElementDescriptor(trackElement.element->AsTrack()->GetTrackType());
const TrackCoordinates* trackCoordinates = &ted.Coordinates;
const auto* trackBlock = ted.Block;
// 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 };
tds.Origin = startPos;
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();
// 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())
{
track.SetFlag(TrackDesignTrackElementFlag::IsInverted);
}
track_elements.push_back(track);
if (!TrackBlockGetNext(&trackElement, &trackElement, nullptr, nullptr))
{
break;
}
z = trackElement.element->GetBaseZ();
direction = trackElement.element->GetDirection();
trackType = trackElement.element->AsTrack()->GetTrackType();
newCoords = GetTrackElementOriginAndApplyChanges(
{ trackElement, z, direction }, trackType, 0, &trackElement.element, 0);
if (!newCoords.has_value())
{
break;
}
trackElement.x = newCoords->x;
trackElement.y = newCoords->y;
if (track_elements.size() > RCT2::Limits::TD6MaxTrackElements)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
} while (trackElement.element != initialMap);
// First entrances, second exits
for (int32_t i = 0; i < 2; i++)
{
for (const auto& station : ride.GetStations())
{
z = station.GetBaseZ();
TileCoordsXYZD location;
if (i == 0)
{
location = station.Entrance;
}
else
{
location = station.Exit;
}
if (location.IsNull())
{
continue;
}
CoordsXY mapLocation = location.ToCoordsXY();
TileElement* tileElement = MapGetFirstElementAt(mapLocation);
if (tileElement == nullptr)
continue;
do
{
if (tileElement->GetType() != TileElementType::Entrance)
continue;
if (tileElement->GetBaseZ() == z)
break;
} while (!(tileElement++)->IsLastForTile());
// Add something that stops this from walking off the end
Direction entranceDirection = tileElement->GetDirection();
entranceDirection -= _saveDirection;
entranceDirection &= kTileElementDirectionMask;
mapLocation -= tds.Origin;
// Rotate entrance coordinates backwards to the correct direction
auto rotatedMapLocation = TileCoordsXY(mapLocation.Rotate(0 - _saveDirection));
z -= tds.Origin.z;
z /= COORDS_Z_STEP;
if (z > 127 || z < -126)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
TrackDesignEntranceElement entrance{};
entrance.Location = TileCoordsXYZD(rotatedMapLocation, z, entranceDirection);
// If this is the exit version
if (i == 1)
{
entrance.IsExit = true;
}
entrance_elements.push_back(entrance);
}
}
TrackDesignPreviewDrawOutlines(tds, this, RideGetTemporaryForPreview(), { 4096, 4096, 0, _currentTrackPieceDirection });
// Resave global vars for scenery reasons.
tds.Origin = startPos;
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_CONSTRUCT;
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_ARROW;
gMapSelectFlags &= ~MAP_SELECT_FLAG_GREEN;
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 };
}
ResultWithMessage TrackDesign::CreateTrackDesignMaze(TrackDesignState& tds, const Ride& ride)
{
auto startLoc = MazeGetFirstElement(ride);
if (startLoc.element == nullptr)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
tds.Origin = { startLoc.x, startLoc.y, startLoc.element->GetBaseZ() };
// 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)
{
for (; x < MAXIMUM_MAP_SIZE_BIG; x += COORDS_XY_STEP)
{
auto tileElement = MapGetFirstElementAt(CoordsXY{ x, y });
do
{
if (tileElement == nullptr)
break;
if (tileElement->GetType() != TileElementType::Track)
continue;
if (tileElement->AsTrack()->GetRideIndex() != ride.id)
continue;
TrackDesignMazeElement maze{};
maze.maze_entry = tileElement->AsTrack()->GetMazeEntry();
maze.x = (x - startLoc.x) / COORDS_XY_STEP;
maze.y = (y - startLoc.y) / COORDS_XY_STEP;
_saveDirection = tileElement->GetDirection();
maze_elements.push_back(maze);
if (maze_elements.size() >= 2000)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
} while (!(tileElement++)->IsLastForTile());
}
x = 0;
}
auto location = ride.GetStation().Entrance;
if (location.IsNull())
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
CoordsXY entranceLoc = location.ToCoordsXY();
auto tileElement = MapGetFirstElementAt(entranceLoc);
do
{
if (tileElement == nullptr)
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
if (tileElement->GetType() != TileElementType::Entrance)
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);
location = ride.GetStation().Exit;
if (location.IsNull())
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
CoordsXY exitLoc = location.ToCoordsXY();
tileElement = MapGetFirstElementAt(exitLoc);
if (tileElement == nullptr)
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
do
{
if (tileElement->GetType() != TileElementType::Entrance)
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);
// Save global vars as they are still used by scenery????
int32_t startZ = tds.Origin.z;
TrackDesignPreviewDrawOutlines(tds, this, RideGetTemporaryForPreview(), { 4096, 4096, 0, _currentTrackPieceDirection });
tds.Origin = { startLoc.x, startLoc.y, startZ };
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_CONSTRUCT;
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_ARROW;
gMapSelectFlags &= ~MAP_SELECT_FLAG_GREEN;
space_required_x = ((tds.PreviewMax.x - tds.PreviewMin.x) / 32) + 1;
space_required_y = ((tds.PreviewMax.y - tds.PreviewMin.y) / 32) + 1;
return { true };
}
CoordsXYE TrackDesign::MazeGetFirstElement(const Ride& ride)
{
CoordsXYE tile{};
for (tile.y = 0; tile.y < MAXIMUM_MAP_SIZE_BIG; tile.y += COORDS_XY_STEP)
{
for (tile.x = 0; tile.x < MAXIMUM_MAP_SIZE_BIG; tile.x += COORDS_XY_STEP)
{
tile.element = MapGetFirstElementAt(CoordsXY{ tile.x, tile.y });
do
{
if (tile.element == nullptr)
break;
if (tile.element->GetType() != TileElementType::Track)
continue;
if (tile.element->AsTrack()->GetRideIndex() == ride.id)
{
return tile;
}
} while (!(tile.element++)->IsLastForTile());
}
}
tile.element = nullptr;
return tile;
}
ResultWithMessage TrackDesign::CreateTrackDesignScenery(TrackDesignState& tds)
{
scenery_elements = _trackSavedTileElementsDesc;
// Run an element loop
for (auto& scenery : scenery_elements)
{
switch (scenery.scenery_object.GetType())
{
case ObjectType::Paths:
{
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:
{
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);
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)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
if (relativeMapPosition.z > 127 * COORDS_Z_STEP || relativeMapPosition.z < -126 * COORDS_Z_STEP)
{
return { false, STR_TRACK_TOO_LARGE_OR_TOO_MUCH_SCENERY };
}
scenery.loc = CoordsXYZ(rotatedRelativeMapPos, relativeMapPosition.z);
}
return { true };
}
void TrackDesign::Serialise(DataSerialiser& stream)
{
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);
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);
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);
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);
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
for (const auto& scenery : td6->scenery_elements)
{
if (scenery.scenery_object.HasValue())
{
objectManager.LoadObject(scenery.scenery_object);
}
}
}
struct TrackSceneryEntry
{
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);
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());
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;
}
if (y2 < tile->y_offset)
{
y2 = tile->y_offset;
}
}
switch (scenery.flags & 3)
{
case 0:
scenery.loc.y = -(scenery.loc.y + y1) - y2;
break;
case 1:
scenery.loc.x = scenery.loc.x + y2 + y1;
scenery.loc.y = -scenery.loc.y;
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;
scenery.flags ^= (1 << 1);
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))
{
scenery.flags ^= (1 << 0);
if (!sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_FULL_TILE))
{
scenery.flags ^= (1 << 2);
}
break;
}
if (scenery.flags & (1 << 0))
{
scenery.flags ^= (1 << 1);
}
scenery.flags ^= (1 << 2);
break;
}
case ObjectType::Walls:
{
scenery.loc.y = -scenery.loc.y;
if (scenery.flags & (1 << 0))
{
scenery.flags ^= (1 << 1);
}
break;
}
case ObjectType::Paths:
case ObjectType::FootpathSurface:
{
scenery.loc.y = -scenery.loc.y;
if (scenery.flags & (1 << 5))
{
scenery.flags ^= (1 << 6);
}
uint8_t flags = scenery.flags;
flags = ((flags & (1 << 3)) >> 2) | ((flags & (1 << 1)) << 2);
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)
{
for (auto& track : td6->track_elements)
{
const auto& ted = GetTrackElementDescriptor(track.Type);
track.Type = ted.MirrorElement;
}
}
/** 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,
};
/**
*
* rct2: 0x006D25FA
*/
static void TrackDesignMirrorMaze(TrackDesign* td6)
{
for (auto& maze : td6->maze_elements)
{
maze.y = -maze.y;
uint16_t maze_entry = maze.maze_entry;
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]);
}
maze.maze_entry = new_entry;
}
}
/**
*
* rct2: 0x006D2436
*/
void TrackDesignMirror(TrackDesign* td6)
{
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)
{
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) };
}
static GameActions::Result TrackDesignPlaceSceneryElementRemoveGhost(
CoordsXY mapCoord, const TrackDesignSceneryElement& scenery, uint8_t rotation, int32_t originZ)
{
auto entryInfo = TrackDesignPlaceSceneryElementGetEntry(scenery);
if (!entryInfo)
{
return GameActions::Result();
}
if (_trackDesignPlaceStateSceneryUnavailable)
{
return GameActions::Result();
}
int32_t z = scenery.loc.z + originZ;
uint8_t sceneryRotation = (rotation + scenery.flags) & kTileElementDirectionMask;
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:
{
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(
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:
ga = std::make_unique<LargeSceneryRemoveAction>(CoordsXYZD{ mapCoord.x, mapCoord.y, z, sceneryRotation }, 0);
break;
case ObjectType::Walls:
ga = std::make_unique<WallRemoveAction>(CoordsXYZD{ mapCoord.x, mapCoord.y, z, sceneryRotation });
break;
case ObjectType::Paths:
case ObjectType::FootpathSurface:
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;
if (z < tds.PlaceSceneryZ)
{
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)
{
if (tds.PlaceOperation == PTD_OPERATION_DRAW_OUTLINES && mode == 0)
{
TrackDesignAddSelectedTile(mapCoord);
return GameActions::Result();
}
if (tds.PlaceOperation == PTD_OPERATION_REMOVE_GHOST && mode == 0)
{
return TrackDesignPlaceSceneryElementRemoveGhost(mapCoord, scenery, rotation, originZ);
}
if (tds.PlaceOperation == PTD_OPERATION_GET_PLACE_Z)
{
TrackDesignPlaceSceneryElementGetPlaceZ(tds, scenery);
return GameActions::Result();
}
money64 cost = 0;
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);
cost = res.Cost;
break;
}
case ObjectType::Walls:
{
if (mode != 0)
{
return GameActions::Result();
}
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)
{
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;
flags = GAME_COMMAND_FLAG_APPLY;
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;
}
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;
}
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)
{
FootpathQueueChainReset();
FootpathRemoveEdgesAt(mapCoord, reinterpret_cast<TileElement*>(pathElement));
}
flags = GAME_COMMAND_FLAG_APPLY;
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.IsReplay)
{
flags |= GAME_COMMAND_FLAG_REPLAY;
}
if (tds.PlaceOperation == PTD_OPERATION_PLACE)
{
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)
{
const auto& origin = tds.Origin;
money64 cost = 0;
for (uint8_t mode = 0; mode <= 1; mode++)
{
if (!sceneryList.empty())
{
tds.HasScenery = true;
}
if (!tds.PlaceScenery)
{
continue;
}
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)
{
// 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)
{
return GameActions::Result(
GameActions::Status::InvalidParameters, STR_ERR_INVALID_PARAMETER, STR_ERR_TILE_ELEMENT_NOT_FOUND);
}
do
{
if (tile_element->GetType() != TileElementType::Track)
{
continue;
}
if (tile_element->GetBaseZ() != newCoords.z)
{
continue;
}
auto stationIndex = tile_element->AsTrack()->GetStationIndex();
uint8_t flags = GAME_COMMAND_FLAG_APPLY;
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;
}
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;
}
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;
totalCost += cost;
}
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)
{
surfaceZ += LAND_HEIGHT_STEP;
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
{
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)
{
tds.PlaceZ -= temp_z;
}
}
}
tds.Origin = origin;
auto result = TrackDesignPlaceEntrances(tds, td, origin, ride.id, totalCost);
if (result.has_value())
{
return result.value();
}
if (tds.PlaceOperation == PTD_OPERATION_REMOVE_GHOST)
{
auto gameAction = RideDemolishAction(ride.id, RIDE_MODIFY_DEMOLISH);
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)
{
tds.Origin = origin;
if (tds.PlaceOperation == PTD_OPERATION_DRAW_OUTLINES)
{
gMapSelectionTiles.clear();
gMapSelectArrowPosition = CoordsXYZ{ origin, TileElementHeight(origin) };
gMapSelectArrowDirection = _currentTrackPieceDirection;
}
tds.PlaceZ = 0;
money64 totalCost = 0;
uint8_t rotation = _currentTrackPieceDirection;
// Track elements
auto newCoords = origin;
for (const auto& track : td6->track_elements)
{
auto trackType = track.Type;
const auto& ted = GetTrackElementDescriptor(trackType);
TrackDesignUpdatePreviewBounds(tds, newCoords);
switch (tds.PlaceOperation)
{
case PTD_OPERATION_DRAW_OUTLINES:
for (const PreviewTrack* trackBlock = ted.Block; trackBlock->index != 0xFF; trackBlock++)
{
auto tile = CoordsXY{ newCoords } + CoordsXY{ trackBlock->x, trackBlock->y }.Rotate(rotation);
TrackDesignUpdatePreviewBounds(tds, { tile, newCoords.z });
TrackDesignAddSelectedTile(tile);
}
break;
case PTD_OPERATION_REMOVE_GHOST:
{
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) });
trackRemoveAction.SetFlags(
GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED | GAME_COMMAND_FLAG_NO_SPEND | GAME_COMMAND_FLAG_GHOST
| GAME_COMMAND_FLAG_TRACK_DESIGN);
GameActions::ExecuteNested(&trackRemoveAction);
break;
}
case PTD_OPERATION_PLACE_QUERY:
case PTD_OPERATION_PLACE:
case PTD_OPERATION_PLACE_GHOST:
case PTD_OPERATION_PLACE_TRACK_PREVIEW:
{
const TrackCoordinates* trackCoordinates = &ted.Coordinates;
// 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;
}
uint8_t flags = GAME_COMMAND_FLAG_APPLY;
if (tds.PlaceOperation == PTD_OPERATION_PLACE_TRACK_PREVIEW)
{
flags |= GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED;
flags |= GAME_COMMAND_FLAG_NO_SPEND;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_GHOST)
{
flags |= GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED;
flags |= GAME_COMMAND_FLAG_NO_SPEND;
flags |= GAME_COMMAND_FLAG_GHOST;
}
else if (tds.PlaceOperation == PTD_OPERATION_PLACE_QUERY)
{
flags = GAME_COMMAND_FLAG_NO_SPEND;
}
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;
break;
}
case PTD_OPERATION_GET_PLACE_Z:
{
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))
{
continue;
}
auto surfaceElement = MapGetSurfaceElementAt(tile);
if (surfaceElement == nullptr)
{
return GameActions::Result(
GameActions::Status::InvalidParameters, STR_ERR_INVALID_PARAMETER,
STR_ERR_SURFACE_ELEMENT_NOT_FOUND);
}
int32_t surfaceZ = surfaceElement->GetBaseZ();
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP)
{
surfaceZ += LAND_HEIGHT_STEP;
if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
{
surfaceZ += LAND_HEIGHT_STEP;
}
}
auto waterZ = surfaceElement->GetWaterHeight();
if (waterZ > 0 && waterZ > surfaceZ)
{
surfaceZ = waterZ;
}
int32_t heightDifference = tempZ + tds.PlaceZ + trackBlock->z - surfaceZ;
if (heightDifference < 0)
{
tds.PlaceZ -= heightDifference;
}
}
break;
}
}
const TrackCoordinates& track_coordinates = ted.Coordinates;
auto offsetAndRotatedTrack = CoordsXY{ newCoords }
+ CoordsXY{ track_coordinates.x, track_coordinates.y }.Rotate(rotation);
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();
}
if (tds.PlaceOperation == PTD_OPERATION_REMOVE_GHOST)
{
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;
tds.PlaceScenery = placeScenery;
tds.EntranceExitPlaced = false;
tds.HasScenery = false;
tds.IsReplay = ptdOperation & PTD_OPERATION_FLAG_IS_REPLAY;
ptdOperation &= ~PTD_OPERATION_FLAG_IS_REPLAY;
tds.PlaceOperation = ptdOperation;
tds.PreviewMin = coords;
tds.PreviewMax = coords;
tds.PlaceSceneryZ = 0;
if (gTrackDesignSceneryToggle)
{
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;
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;
}
// Scenery elements
auto sceneryPlaceRes = TrackDesignPlaceAllScenery(tds, td6->scenery_elements, coords.direction);
if (sceneryPlaceRes.Error != GameActions::Status::Ok)
{
return sceneryPlaceRes;
}
// 0x6D0FE6
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;
return res;
}
GameActions::Result TrackDesignPlace(TrackDesign* td6, uint32_t flags, bool placeScenery, Ride& ride, const CoordsXYZD& coords)
{
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;
TrackDesignState tds{};
return TrackDesignPlaceVirtual(tds, td6, ptdOperation, placeScenery, ride, coords);
}
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)
{
// Don't set colours as will be set correctly later.
auto gameAction = RideCreateAction(type, subType, 0, 0, GetGameState().LastEntranceStyle);
gameAction.SetFlags(flags);
auto res = GameActions::ExecuteNested(&gameAction);
// Callee's of this function expect kMoney64Undefined in case of failure.
if (res.Error != GameActions::Status::Ok)
{
return kMoney64Undefined;
}
*outRideIndex = res.GetData<RideId>();
return res.Cost;
}
/**
*
* rct2: 0x006D2189
* ebx = ride_id
* cost = edi
*/
static bool TrackDesignPlacePreview(TrackDesignState& tds, TrackDesign* td6, money64* cost, Ride** outRide, uint8_t* flags)
{
*outRide = nullptr;
*flags = 0;
auto& gameState = GetGameState();
auto& objManager = GetContext()->GetObjectManager();
auto entry_index = objManager.GetLoadedObjectEntryIndex(td6->vehicle_object);
RideId rideIndex;
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;
ride->custom_name = {};
ride->entrance_style = objManager.GetLoadedObjectEntryIndex(td6->StationObjectIdentifier);
if (ride->entrance_style == OBJECT_ENTRY_INDEX_NULL)
{
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];
}
}
_trackDesignDrawingPreview = true;
uint8_t backup_rotation = _currentTrackPieceDirection;
uint32_t backup_park_flags = gameState.Park.Flags;
gameState.Park.Flags &= ~PARK_FLAGS_FORBID_HIGH_CONSTRUCTION;
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 });
if (tds.HasScenery)
{
*flags |= TRACK_DESIGN_FLAG_HAS_SCENERY;
}
z += 16 - tds.PlaceSceneryZ;
bool placeScenery = true;
if (_trackDesignPlaceStateSceneryUnavailable)
{
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;
}
else if (!RideEntryIsInvented(entry_index) && !GetGameState().Cheats.IgnoreResearchStatus)
{
*flags |= TRACK_DESIGN_FLAG_VEHICLE_UNAVAILABLE;
}
_currentTrackPieceDirection = backup_rotation;
_trackDesignDrawingPreview = false;
*cost = res.Cost;
*outRide = ride;
return true;
}
_currentTrackPieceDirection = backup_rotation;
ride->Delete();
_trackDesignDrawingPreview = false;
return false;
}
#pragma region Track Design Preview
/**
*
* rct2: 0x006D1EF0
*/
void TrackDesignDrawPreview(TrackDesign* td6, uint8_t* pixels)
{
StashMap();
TrackDesignPreviewClearMap();
if (gScreenFlags & SCREEN_FLAGS_TRACK_MANAGER)
{
TrackDesignLoadSceneryObjects(td6);
}
TrackDesignState tds{};
money64 cost;
Ride* ride;
uint8_t flags;
if (!TrackDesignPlacePreview(tds, td6, &cost, &ride, &flags))
{
std::fill_n(pixels, kTrackPreviewImageSize * 4, 0x00);
UnstashMap();
return;
}
td6->cost = cost;
td6->track_flags = flags & 7;
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 };
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;
}
ZoomLevel zoom_level{ 1 };
if (size_x < size_y)
{
size_x = size_y;
}
if (size_x > 1000 || size_z > 280)
{
zoom_level = ZoomLevel{ 2 };
}
if (size_x > 1600 || size_z > 1000)
{
zoom_level = ZoomLevel{ 3 };
}
size_x = zoom_level.ApplyTo(370);
size_y = zoom_level.ApplyTo(217);
Viewport view;
view.width = 370;
view.height = 217;
view.view_width = size_x;
view.view_height = size_y;
view.pos = { 0, 0 };
view.zoom = zoom_level;
view.flags = VIEWPORT_FLAG_HIDE_BASE | VIEWPORT_FLAG_HIDE_ENTITIES;
DrawPixelInfo dpi;
dpi.zoom_level = zoom_level;
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;
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++)
{
auto* element = &tileElements.emplace_back();
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