mirror of https://github.com/OpenRCT2/OpenRCT2.git
6001 lines
191 KiB
C++
6001 lines
191 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 "Ride.h"
|
||
|
||
#include "../Cheats.h"
|
||
#include "../Context.h"
|
||
#include "../Editor.h"
|
||
#include "../Game.h"
|
||
#include "../GameState.h"
|
||
#include "../Input.h"
|
||
#include "../OpenRCT2.h"
|
||
#include "../actions/ResultWithMessage.h"
|
||
#include "../actions/RideSetSettingAction.h"
|
||
#include "../actions/RideSetStatusAction.h"
|
||
#include "../actions/RideSetVehicleAction.h"
|
||
#include "../audio/AudioMixer.h"
|
||
#include "../audio/audio.h"
|
||
#include "../common.h"
|
||
#include "../config/Config.h"
|
||
#include "../core/BitSet.hpp"
|
||
#include "../core/FixedVector.h"
|
||
#include "../core/Guard.hpp"
|
||
#include "../core/Numerics.hpp"
|
||
#include "../entity/EntityRegistry.h"
|
||
#include "../entity/Peep.h"
|
||
#include "../entity/Staff.h"
|
||
#include "../interface/Window_internal.h"
|
||
#include "../localisation/Date.h"
|
||
#include "../localisation/Formatter.h"
|
||
#include "../localisation/Formatting.h"
|
||
#include "../localisation/Localisation.h"
|
||
#include "../management/Finance.h"
|
||
#include "../management/Marketing.h"
|
||
#include "../management/NewsItem.h"
|
||
#include "../network/network.h"
|
||
#include "../object/MusicObject.h"
|
||
#include "../object/ObjectList.h"
|
||
#include "../object/ObjectManager.h"
|
||
#include "../object/RideObject.h"
|
||
#include "../object/StationObject.h"
|
||
#include "../paint/VirtualFloor.h"
|
||
#include "../profiling/Profiling.h"
|
||
#include "../rct1/RCT1.h"
|
||
#include "../scenario/Scenario.h"
|
||
#include "../ui/UiContext.h"
|
||
#include "../ui/WindowManager.h"
|
||
#include "../util/Util.h"
|
||
#include "../windows/Intent.h"
|
||
#include "../world/Banner.h"
|
||
#include "../world/Climate.h"
|
||
#include "../world/Entrance.h"
|
||
#include "../world/Footpath.h"
|
||
#include "../world/Location.hpp"
|
||
#include "../world/Map.h"
|
||
#include "../world/MapAnimation.h"
|
||
#include "../world/Park.h"
|
||
#include "../world/Scenery.h"
|
||
#include "../world/TileElementsView.h"
|
||
#include "CableLift.h"
|
||
#include "RideAudio.h"
|
||
#include "RideConstruction.h"
|
||
#include "RideData.h"
|
||
#include "RideEntry.h"
|
||
#include "ShopItem.h"
|
||
#include "Station.h"
|
||
#include "Track.h"
|
||
#include "TrackData.h"
|
||
#include "TrackDesign.h"
|
||
#include "TrainManager.h"
|
||
#include "Vehicle.h"
|
||
|
||
#include <algorithm>
|
||
#include <cassert>
|
||
#include <climits>
|
||
#include <cstdlib>
|
||
#include <iterator>
|
||
#include <limits>
|
||
#include <optional>
|
||
|
||
using namespace OpenRCT2;
|
||
using namespace OpenRCT2::TrackMetaData;
|
||
|
||
RideMode& operator++(RideMode& d, int)
|
||
{
|
||
return d = (d == RideMode::Count) ? RideMode::Normal : static_cast<RideMode>(static_cast<uint8_t>(d) + 1);
|
||
}
|
||
|
||
static constexpr int32_t RideInspectionInterval[] = {
|
||
10, 20, 30, 45, 60, 120, 0, 0,
|
||
};
|
||
|
||
// This is the highest used index + 1 of the GameState_t::Rides array.
|
||
static size_t _endOfUsedRange = 0;
|
||
|
||
// A special instance of Ride that is used to draw previews such as the track designs.
|
||
static Ride _previewRide{};
|
||
|
||
struct StationIndexWithMessage
|
||
{
|
||
::StationIndex StationIndex;
|
||
StringId Message = STR_NONE;
|
||
};
|
||
|
||
// Static function declarations
|
||
Staff* FindClosestMechanic(const CoordsXY& entrancePosition, int32_t forInspection);
|
||
static void RideBreakdownStatusUpdate(Ride& ride);
|
||
static void RideBreakdownUpdate(Ride& ride);
|
||
static void RideCallClosestMechanic(Ride& ride);
|
||
static void RideCallMechanic(Ride& ride, Peep* mechanic, int32_t forInspection);
|
||
static void RideEntranceExitConnected(Ride& ride);
|
||
static int32_t RideGetNewBreakdownProblem(const Ride& ride);
|
||
static void RideInspectionUpdate(Ride& ride);
|
||
static void RideMechanicStatusUpdate(Ride& ride, int32_t mechanicStatus);
|
||
static void RideMusicUpdate(Ride& ride);
|
||
static void RideShopConnected(const Ride& ride);
|
||
|
||
RideManager GetRideManager()
|
||
{
|
||
return {};
|
||
}
|
||
|
||
size_t RideManager::size() const
|
||
{
|
||
auto& gameState = GetGameState();
|
||
size_t count = 0;
|
||
for (size_t i = 0; i < _endOfUsedRange; i++)
|
||
{
|
||
if (!gameState.Rides[i].id.IsNull())
|
||
{
|
||
count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
RideManager::Iterator RideManager::begin()
|
||
{
|
||
return RideManager::Iterator(*this, 0u, _endOfUsedRange);
|
||
}
|
||
|
||
RideManager::Iterator RideManager::end()
|
||
{
|
||
return RideManager::Iterator(*this, _endOfUsedRange, _endOfUsedRange);
|
||
}
|
||
|
||
RideManager::Iterator RideManager::get(RideId rideId)
|
||
{
|
||
return RideManager::Iterator(*this, rideId.ToUnderlying(), _endOfUsedRange);
|
||
}
|
||
|
||
RideId GetNextFreeRideId()
|
||
{
|
||
auto& gameState = GetGameState();
|
||
for (RideId::UnderlyingType i = 0; i < gameState.Rides.size(); i++)
|
||
{
|
||
if (gameState.Rides[i].id.IsNull())
|
||
{
|
||
return RideId::FromUnderlying(i);
|
||
}
|
||
}
|
||
return RideId::GetNull();
|
||
}
|
||
|
||
Ride* RideAllocateAtIndex(RideId index)
|
||
{
|
||
const auto idx = index.ToUnderlying();
|
||
_endOfUsedRange = std::max<size_t>(idx + 1, _endOfUsedRange);
|
||
|
||
auto result = &GetGameState().Rides[idx];
|
||
assert(result->id == RideId::GetNull());
|
||
|
||
// Initialize the ride to all the defaults.
|
||
*result = Ride{};
|
||
|
||
// Because it is default initialized to zero rather than the magic constant for Null, fill the array.
|
||
std::fill(std::begin(result->vehicles), std::end(result->vehicles), EntityId::GetNull());
|
||
|
||
result->id = index;
|
||
return result;
|
||
}
|
||
|
||
Ride& RideGetTemporaryForPreview()
|
||
{
|
||
return _previewRide;
|
||
}
|
||
|
||
static void RideReset(Ride& ride)
|
||
{
|
||
ride.id = RideId::GetNull();
|
||
ride.type = RIDE_TYPE_NULL;
|
||
ride.custom_name = {};
|
||
ride.measurement = {};
|
||
}
|
||
|
||
void RideDelete(RideId id)
|
||
{
|
||
auto& gameState = GetGameState();
|
||
const auto idx = id.ToUnderlying();
|
||
|
||
assert(idx < gameState.Rides.size());
|
||
assert(gameState.Rides[idx].type != RIDE_TYPE_NULL);
|
||
|
||
auto& ride = gameState.Rides[idx];
|
||
RideReset(ride);
|
||
|
||
// Shrink maximum ride size.
|
||
while (_endOfUsedRange > 0 && gameState.Rides[_endOfUsedRange - 1].id.IsNull())
|
||
{
|
||
_endOfUsedRange--;
|
||
}
|
||
}
|
||
|
||
Ride* GetRide(RideId index)
|
||
{
|
||
if (index.IsNull())
|
||
{
|
||
return nullptr;
|
||
}
|
||
|
||
auto& gameState = GetGameState();
|
||
const auto idx = index.ToUnderlying();
|
||
assert(idx < gameState.Rides.size());
|
||
if (idx >= gameState.Rides.size())
|
||
{
|
||
return nullptr;
|
||
}
|
||
|
||
auto& ride = gameState.Rides[idx];
|
||
if (ride.type != RIDE_TYPE_NULL)
|
||
{
|
||
assert(ride.id == index);
|
||
return &ride;
|
||
}
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
const RideObjectEntry* GetRideEntryByIndex(ObjectEntryIndex index)
|
||
{
|
||
auto& objMgr = OpenRCT2::GetContext()->GetObjectManager();
|
||
|
||
auto obj = objMgr.GetLoadedObject(ObjectType::Ride, index);
|
||
if (obj == nullptr)
|
||
{
|
||
return nullptr;
|
||
}
|
||
|
||
return static_cast<RideObjectEntry*>(obj->GetLegacyData());
|
||
}
|
||
|
||
std::string_view GetRideEntryName(ObjectEntryIndex index)
|
||
{
|
||
if (index >= object_entry_group_counts[EnumValue(ObjectType::Ride)])
|
||
{
|
||
LOG_ERROR("invalid index %d for ride type", index);
|
||
return {};
|
||
}
|
||
|
||
auto objectEntry = ObjectEntryGetObject(ObjectType::Ride, index);
|
||
if (objectEntry != nullptr)
|
||
{
|
||
return objectEntry->GetLegacyIdentifier();
|
||
}
|
||
return {};
|
||
}
|
||
|
||
const RideObjectEntry* Ride::GetRideEntry() const
|
||
{
|
||
return GetRideEntryByIndex(subtype);
|
||
}
|
||
|
||
int32_t RideGetCount()
|
||
{
|
||
return static_cast<int32_t>(GetRideManager().size());
|
||
}
|
||
|
||
size_t Ride::GetNumPrices() const
|
||
{
|
||
size_t result = 0;
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_CASH_MACHINE) || rtd.HasFlag(RIDE_TYPE_FLAG_IS_FIRST_AID))
|
||
{
|
||
result = 0;
|
||
}
|
||
else if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_TOILET))
|
||
{
|
||
result = 1;
|
||
}
|
||
else
|
||
{
|
||
result = 1;
|
||
|
||
auto rideEntry = GetRideEntry();
|
||
if (rideEntry != nullptr)
|
||
{
|
||
if (lifecycle_flags & RIDE_LIFECYCLE_ON_RIDE_PHOTO)
|
||
{
|
||
result++;
|
||
}
|
||
else if (rideEntry->shop_item[1] != ShopItem::None)
|
||
{
|
||
result++;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
int32_t Ride::GetAge() const
|
||
{
|
||
return GetDate().GetMonthsElapsed() - build_date;
|
||
}
|
||
|
||
int32_t Ride::GetTotalQueueLength() const
|
||
{
|
||
int32_t queueLength = 0;
|
||
for (const auto& station : stations)
|
||
if (!station.Entrance.IsNull())
|
||
queueLength += station.QueueLength;
|
||
return queueLength;
|
||
}
|
||
|
||
int32_t Ride::GetMaxQueueTime() const
|
||
{
|
||
uint8_t queueTime = 0;
|
||
for (const auto& station : stations)
|
||
if (!station.Entrance.IsNull())
|
||
queueTime = std::max(queueTime, station.QueueTime);
|
||
return static_cast<int32_t>(queueTime);
|
||
}
|
||
|
||
Guest* Ride::GetQueueHeadGuest(StationIndex stationIndex) const
|
||
{
|
||
Guest* peep;
|
||
Guest* result = nullptr;
|
||
auto spriteIndex = GetStation(stationIndex).LastPeepInQueue;
|
||
while ((peep = TryGetEntity<Guest>(spriteIndex)) != nullptr)
|
||
{
|
||
spriteIndex = peep->GuestNextInQueue;
|
||
result = peep;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
void Ride::UpdateQueueLength(StationIndex stationIndex)
|
||
{
|
||
uint16_t count = 0;
|
||
Guest* peep;
|
||
auto& station = GetStation(stationIndex);
|
||
auto spriteIndex = station.LastPeepInQueue;
|
||
while ((peep = TryGetEntity<Guest>(spriteIndex)) != nullptr)
|
||
{
|
||
spriteIndex = peep->GuestNextInQueue;
|
||
count++;
|
||
}
|
||
station.QueueLength = count;
|
||
}
|
||
|
||
void Ride::QueueInsertGuestAtFront(StationIndex stationIndex, Guest* peep)
|
||
{
|
||
assert(stationIndex.ToUnderlying() < OpenRCT2::Limits::MaxStationsPerRide);
|
||
assert(peep != nullptr);
|
||
|
||
peep->GuestNextInQueue = EntityId::GetNull();
|
||
auto* queueHeadGuest = GetQueueHeadGuest(peep->CurrentRideStation);
|
||
if (queueHeadGuest == nullptr)
|
||
{
|
||
GetStation(peep->CurrentRideStation).LastPeepInQueue = peep->Id;
|
||
}
|
||
else
|
||
{
|
||
queueHeadGuest->GuestNextInQueue = peep->Id;
|
||
}
|
||
UpdateQueueLength(peep->CurrentRideStation);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC916
|
||
*/
|
||
void RideUpdateFavouritedStat()
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
ride.guests_favourite = 0;
|
||
|
||
for (auto peep : EntityList<Guest>())
|
||
{
|
||
if (!peep->FavouriteRide.IsNull())
|
||
{
|
||
auto ride = GetRide(peep->FavouriteRide);
|
||
if (ride != nullptr)
|
||
{
|
||
ride->guests_favourite++;
|
||
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
||
}
|
||
}
|
||
}
|
||
|
||
WindowInvalidateByClass(WindowClass::RideList);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC3AB
|
||
*/
|
||
money64 Ride::CalculateIncomePerHour() const
|
||
{
|
||
// Get entry by ride to provide better reporting
|
||
const auto* entry = GetRideEntry();
|
||
if (entry == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
auto customersPerHour = RideCustomersPerHour(*this);
|
||
money64 priceMinusCost = RideGetPrice(*this);
|
||
|
||
ShopItem currentShopItem = entry->shop_item[0];
|
||
if (currentShopItem != ShopItem::None)
|
||
{
|
||
priceMinusCost -= GetShopItemDescriptor(currentShopItem).Cost;
|
||
}
|
||
|
||
currentShopItem = (lifecycle_flags & RIDE_LIFECYCLE_ON_RIDE_PHOTO) ? GetRideTypeDescriptor().PhotoItem
|
||
: entry->shop_item[1];
|
||
|
||
if (currentShopItem != ShopItem::None)
|
||
{
|
||
const money64 shopItemProfit = price[1] - GetShopItemDescriptor(currentShopItem).Cost;
|
||
|
||
if (GetShopItemDescriptor(currentShopItem).IsPhoto())
|
||
{
|
||
const int32_t rideTicketsSold = total_customers - no_secondary_items_sold;
|
||
|
||
// Use the ratio between photo sold and total admissions to approximate the photo income(as not every guest will buy
|
||
// one).
|
||
// TODO: use data from the last 5 minutes instead of all-time values for a more accurate calculation
|
||
if (rideTicketsSold > 0)
|
||
{
|
||
priceMinusCost += ((static_cast<int32_t>(no_secondary_items_sold) * shopItemProfit) / rideTicketsSold);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
priceMinusCost += shopItemProfit;
|
||
}
|
||
|
||
if (entry->shop_item[0] != ShopItem::None)
|
||
priceMinusCost /= 2;
|
||
}
|
||
|
||
return customersPerHour * priceMinusCost;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006CAF80
|
||
* ax result x
|
||
* bx result y
|
||
* dl ride index
|
||
* esi result map element
|
||
*/
|
||
bool RideTryGetOriginElement(const Ride& ride, CoordsXYE* output)
|
||
{
|
||
TileElement* resultTileElement = nullptr;
|
||
|
||
TileElementIterator it;
|
||
TileElementIteratorBegin(&it);
|
||
do
|
||
{
|
||
if (it.element->GetType() != TileElementType::Track)
|
||
continue;
|
||
if (it.element->AsTrack()->GetRideIndex() != ride.id)
|
||
continue;
|
||
|
||
// Found a track piece for target ride
|
||
|
||
// Check if it's not the station or ??? (but allow end piece of station)
|
||
const auto& ted = GetTrackElementDescriptor(it.element->AsTrack()->GetTrackType());
|
||
bool specialTrackPiece
|
||
= (it.element->AsTrack()->GetTrackType() != TrackElemType::BeginStation
|
||
&& it.element->AsTrack()->GetTrackType() != TrackElemType::MiddleStation
|
||
&& (std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN));
|
||
|
||
// Set result tile to this track piece if first found track or a ???
|
||
if (resultTileElement == nullptr || specialTrackPiece)
|
||
{
|
||
resultTileElement = it.element;
|
||
|
||
if (output != nullptr)
|
||
{
|
||
output->element = resultTileElement;
|
||
output->x = it.x * COORDS_XY_STEP;
|
||
output->y = it.y * COORDS_XY_STEP;
|
||
}
|
||
}
|
||
|
||
if (specialTrackPiece)
|
||
{
|
||
return true;
|
||
}
|
||
} while (TileElementIteratorNext(&it));
|
||
|
||
return resultTileElement != nullptr;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006C6096
|
||
* Gets the next track block coordinates from the
|
||
* coordinates of the first of element of a track block.
|
||
* Use track_block_get_next if you are unsure if you are
|
||
* on the first element of a track block
|
||
*/
|
||
bool TrackBlockGetNextFromZero(
|
||
const CoordsXYZ& startPos, const Ride& ride, uint8_t direction_start, CoordsXYE* output, int32_t* z, int32_t* direction,
|
||
bool isGhost)
|
||
{
|
||
auto trackPos = startPos;
|
||
|
||
if (!(direction_start & TRACK_BLOCK_2))
|
||
{
|
||
trackPos += CoordsDirectionDelta[direction_start];
|
||
}
|
||
|
||
TileElement* tileElement = MapGetFirstElementAt(trackPos);
|
||
if (tileElement == nullptr)
|
||
{
|
||
output->element = nullptr;
|
||
output->x = LOCATION_NULL;
|
||
return false;
|
||
}
|
||
|
||
do
|
||
{
|
||
auto trackElement = tileElement->AsTrack();
|
||
if (trackElement == nullptr)
|
||
continue;
|
||
|
||
if (trackElement->GetRideIndex() != ride.id)
|
||
continue;
|
||
|
||
if (trackElement->GetSequenceIndex() != 0)
|
||
continue;
|
||
|
||
if (tileElement->IsGhost() != isGhost)
|
||
continue;
|
||
|
||
const auto& ted = GetTrackElementDescriptor(trackElement->GetTrackType());
|
||
const auto* nextTrackBlock = ted.Block;
|
||
if (nextTrackBlock == nullptr)
|
||
continue;
|
||
|
||
const auto& nextTrackCoordinate = ted.Coordinates;
|
||
uint8_t nextRotation = tileElement->GetDirectionWithOffset(nextTrackCoordinate.rotation_begin)
|
||
| (nextTrackCoordinate.rotation_begin & TRACK_BLOCK_2);
|
||
|
||
if (nextRotation != direction_start)
|
||
continue;
|
||
|
||
int16_t nextZ = nextTrackCoordinate.z_begin - nextTrackBlock->z + tileElement->GetBaseZ();
|
||
if (nextZ != trackPos.z)
|
||
continue;
|
||
|
||
if (z != nullptr)
|
||
*z = tileElement->GetBaseZ();
|
||
if (direction != nullptr)
|
||
*direction = nextRotation;
|
||
*output = { trackPos, tileElement };
|
||
return true;
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
|
||
if (direction != nullptr)
|
||
*direction = direction_start;
|
||
if (z != nullptr)
|
||
*z = trackPos.z;
|
||
*output = { trackPos, --tileElement };
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006C60C2
|
||
*/
|
||
bool TrackBlockGetNext(CoordsXYE* input, CoordsXYE* output, int32_t* z, int32_t* direction)
|
||
{
|
||
if (input == nullptr || input->element == nullptr)
|
||
return false;
|
||
|
||
auto inputElement = input->element->AsTrack();
|
||
if (inputElement == nullptr)
|
||
return false;
|
||
|
||
auto rideIndex = inputElement->GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
const auto& ted = GetTrackElementDescriptor(inputElement->GetTrackType());
|
||
const auto* trackBlock = ted.GetBlockForSequence(inputElement->GetSequenceIndex());
|
||
if (trackBlock == nullptr)
|
||
return false;
|
||
|
||
const auto& trackCoordinate = ted.Coordinates;
|
||
|
||
int32_t x = input->x;
|
||
int32_t y = input->y;
|
||
int32_t OriginZ = inputElement->GetBaseZ();
|
||
|
||
uint8_t rotation = inputElement->GetDirection();
|
||
|
||
CoordsXY coords = { x, y };
|
||
CoordsXY trackCoordOffset = { trackCoordinate.x, trackCoordinate.y };
|
||
CoordsXY trackBlockOffset = { trackBlock->x, trackBlock->y };
|
||
coords += trackCoordOffset.Rotate(rotation);
|
||
coords += trackBlockOffset.Rotate(DirectionReverse(rotation));
|
||
|
||
OriginZ -= trackBlock->z;
|
||
OriginZ += trackCoordinate.z_end;
|
||
|
||
uint8_t directionStart = ((trackCoordinate.rotation_end + rotation) & kTileElementDirectionMask)
|
||
| (trackCoordinate.rotation_end & TRACK_BLOCK_2);
|
||
|
||
return TrackBlockGetNextFromZero({ coords, OriginZ }, *ride, directionStart, output, z, direction, false);
|
||
}
|
||
|
||
/**
|
||
* Returns the begin position / direction and end position / direction of the
|
||
* track piece that proceeds the given location. Gets the previous track block
|
||
* coordinates from the coordinates of the first of element of a track block.
|
||
* Use track_block_get_previous if you are unsure if you are on the first
|
||
* element of a track block
|
||
* rct2: 0x006C63D6
|
||
*/
|
||
bool TrackBlockGetPreviousFromZero(
|
||
const CoordsXYZ& startPos, const Ride& ride, uint8_t direction, TrackBeginEnd* outTrackBeginEnd)
|
||
{
|
||
uint8_t directionStart = direction;
|
||
direction = DirectionReverse(direction);
|
||
auto trackPos = startPos;
|
||
|
||
if (!(direction & TRACK_BLOCK_2))
|
||
{
|
||
trackPos += CoordsDirectionDelta[direction];
|
||
}
|
||
|
||
TileElement* tileElement = MapGetFirstElementAt(trackPos);
|
||
if (tileElement == nullptr)
|
||
{
|
||
outTrackBeginEnd->end_x = trackPos.x;
|
||
outTrackBeginEnd->end_y = trackPos.y;
|
||
outTrackBeginEnd->begin_element = nullptr;
|
||
outTrackBeginEnd->begin_direction = DirectionReverse(directionStart);
|
||
return false;
|
||
}
|
||
|
||
do
|
||
{
|
||
auto trackElement = tileElement->AsTrack();
|
||
if (trackElement == nullptr)
|
||
continue;
|
||
|
||
if (trackElement->GetRideIndex() != ride.id)
|
||
continue;
|
||
|
||
const auto* ted = &GetTrackElementDescriptor(trackElement->GetTrackType());
|
||
const auto& nextTrackCoordinate = ted->Coordinates;
|
||
|
||
const auto* nextTrackBlock = ted->GetBlockForSequence(trackElement->GetSequenceIndex());
|
||
if (nextTrackBlock == nullptr)
|
||
continue;
|
||
if ((nextTrackBlock + 1)->index != 255)
|
||
continue;
|
||
|
||
uint8_t nextRotation = tileElement->GetDirectionWithOffset(nextTrackCoordinate.rotation_end)
|
||
| (nextTrackCoordinate.rotation_end & TRACK_BLOCK_2);
|
||
|
||
if (nextRotation != directionStart)
|
||
continue;
|
||
|
||
int16_t nextZ = nextTrackCoordinate.z_end - nextTrackBlock->z + tileElement->GetBaseZ();
|
||
if (nextZ != trackPos.z)
|
||
continue;
|
||
|
||
nextRotation = tileElement->GetDirectionWithOffset(nextTrackCoordinate.rotation_begin)
|
||
| (nextTrackCoordinate.rotation_begin & TRACK_BLOCK_2);
|
||
outTrackBeginEnd->begin_element = tileElement;
|
||
outTrackBeginEnd->begin_x = trackPos.x;
|
||
outTrackBeginEnd->begin_y = trackPos.y;
|
||
outTrackBeginEnd->end_x = trackPos.x;
|
||
outTrackBeginEnd->end_y = trackPos.y;
|
||
|
||
CoordsXY coords = { outTrackBeginEnd->begin_x, outTrackBeginEnd->begin_y };
|
||
CoordsXY offsets = { nextTrackCoordinate.x, nextTrackCoordinate.y };
|
||
coords += offsets.Rotate(DirectionReverse(nextRotation));
|
||
outTrackBeginEnd->begin_x = coords.x;
|
||
outTrackBeginEnd->begin_y = coords.y;
|
||
|
||
outTrackBeginEnd->begin_z = tileElement->GetBaseZ();
|
||
|
||
ted = &GetTrackElementDescriptor(trackElement->GetTrackType());
|
||
const auto* nextTrackBlock2 = ted->Block;
|
||
if (nextTrackBlock2 == nullptr)
|
||
continue;
|
||
|
||
outTrackBeginEnd->begin_z += nextTrackBlock2->z - nextTrackBlock->z;
|
||
outTrackBeginEnd->begin_direction = nextRotation;
|
||
outTrackBeginEnd->end_direction = DirectionReverse(directionStart);
|
||
return true;
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
|
||
outTrackBeginEnd->end_x = trackPos.x;
|
||
outTrackBeginEnd->end_y = trackPos.y;
|
||
outTrackBeginEnd->begin_z = trackPos.z;
|
||
outTrackBeginEnd->begin_element = nullptr;
|
||
outTrackBeginEnd->end_direction = DirectionReverse(directionStart);
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006C6402
|
||
*
|
||
* @remarks outTrackBeginEnd.begin_x and outTrackBeginEnd.begin_y will be in the
|
||
* higher two bytes of ecx and edx where as outTrackBeginEnd.end_x and
|
||
* outTrackBeginEnd.end_y will be in the lower two bytes (cx and dx).
|
||
*/
|
||
bool TrackBlockGetPrevious(const CoordsXYE& trackPos, TrackBeginEnd* outTrackBeginEnd)
|
||
{
|
||
if (trackPos.element == nullptr)
|
||
return false;
|
||
|
||
auto trackElement = trackPos.element->AsTrack();
|
||
if (trackElement == nullptr)
|
||
return false;
|
||
|
||
const auto& ted = GetTrackElementDescriptor(trackElement->GetTrackType());
|
||
|
||
auto rideIndex = trackElement->GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
const auto* trackBlock = ted.GetBlockForSequence(trackElement->GetSequenceIndex());
|
||
if (trackBlock == nullptr)
|
||
return false;
|
||
|
||
auto trackCoordinate = ted.Coordinates;
|
||
|
||
int32_t z = trackElement->GetBaseZ();
|
||
|
||
uint8_t rotation = trackElement->GetDirection();
|
||
CoordsXY coords = CoordsXY{ trackPos };
|
||
CoordsXY offsets = { trackBlock->x, trackBlock->y };
|
||
coords += offsets.Rotate(DirectionReverse(rotation));
|
||
|
||
z -= trackBlock->z;
|
||
z += trackCoordinate.z_begin;
|
||
|
||
rotation = ((trackCoordinate.rotation_begin + rotation) & kTileElementDirectionMask)
|
||
| (trackCoordinate.rotation_begin & TRACK_BLOCK_2);
|
||
|
||
return TrackBlockGetPreviousFromZero({ coords, z }, *ride, rotation, outTrackBeginEnd);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Make sure to pass in the x and y of the start track element too.
|
||
* rct2: 0x006CB02F
|
||
* ax result x
|
||
* bx result y
|
||
* esi input / output map element
|
||
*/
|
||
bool Ride::FindTrackGap(const CoordsXYE& input, CoordsXYE* output) const
|
||
{
|
||
if (input.element == nullptr || input.element->GetType() != TileElementType::Track)
|
||
return false;
|
||
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
return false;
|
||
|
||
WindowBase* w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && _currentRideIndex == id)
|
||
{
|
||
RideConstructionInvalidateCurrentTrack();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
TrackCircuitIterator it = {};
|
||
TrackCircuitIteratorBegin(&it, input);
|
||
TrackCircuitIterator slowIt = it;
|
||
while (TrackCircuitIteratorNext(&it))
|
||
{
|
||
if (!TrackIsConnectedByShape(it.last.element, it.current.element))
|
||
{
|
||
*output = it.current;
|
||
return true;
|
||
}
|
||
// #2081: prevent an infinite loop
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
TrackCircuitIteratorNext(&slowIt);
|
||
if (TrackCircuitIteratorsMatch(&it, &slowIt))
|
||
{
|
||
*output = it.current;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
if (!it.looped)
|
||
{
|
||
*output = it.last;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
void Ride::FormatStatusTo(Formatter& ft) const
|
||
{
|
||
if (lifecycle_flags & RIDE_LIFECYCLE_CRASHED)
|
||
{
|
||
ft.Add<StringId>(STR_CRASHED);
|
||
}
|
||
else if (lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
|
||
{
|
||
ft.Add<StringId>(STR_BROKEN_DOWN);
|
||
}
|
||
else if (status == RideStatus::Closed)
|
||
{
|
||
if (!GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP_OR_FACILITY))
|
||
{
|
||
if (num_riders != 0)
|
||
{
|
||
ft.Add<StringId>(num_riders == 1 ? STR_CLOSED_WITH_PERSON : STR_CLOSED_WITH_PEOPLE);
|
||
ft.Add<uint16_t>(num_riders);
|
||
}
|
||
else
|
||
{
|
||
ft.Add<StringId>(STR_CLOSED);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ft.Add<StringId>(STR_CLOSED);
|
||
}
|
||
}
|
||
else if (status == RideStatus::Simulating)
|
||
{
|
||
ft.Add<StringId>(STR_SIMULATING);
|
||
}
|
||
else if (status == RideStatus::Testing)
|
||
{
|
||
ft.Add<StringId>(STR_TEST_RUN);
|
||
}
|
||
else if (mode == RideMode::Race && !(lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING) && !race_winner.IsNull())
|
||
{
|
||
auto peep = GetEntity<Guest>(race_winner);
|
||
if (peep != nullptr)
|
||
{
|
||
ft.Add<StringId>(STR_RACE_WON_BY);
|
||
peep->FormatNameTo(ft);
|
||
}
|
||
else
|
||
{
|
||
ft.Add<StringId>(STR_RACE_WON_BY);
|
||
ft.Add<StringId>(STR_NONE);
|
||
}
|
||
}
|
||
else if (!GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP_OR_FACILITY))
|
||
{
|
||
ft.Add<StringId>(num_riders == 1 ? STR_PERSON_ON_RIDE : STR_PEOPLE_ON_RIDE);
|
||
ft.Add<uint16_t>(num_riders);
|
||
}
|
||
else
|
||
{
|
||
ft.Add<StringId>(STR_OPEN);
|
||
}
|
||
}
|
||
|
||
int32_t Ride::GetTotalLength() const
|
||
{
|
||
int32_t i, totalLength = 0;
|
||
for (i = 0; i < num_stations; i++)
|
||
totalLength += stations[i].SegmentLength;
|
||
return totalLength;
|
||
}
|
||
|
||
int32_t Ride::GetTotalTime() const
|
||
{
|
||
int32_t i, totalTime = 0;
|
||
for (i = 0; i < num_stations; i++)
|
||
totalTime += stations[i].SegmentTime;
|
||
return totalTime;
|
||
}
|
||
|
||
bool Ride::CanHaveMultipleCircuits() const
|
||
{
|
||
if (!(GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_ALLOW_MULTIPLE_CIRCUITS)))
|
||
return false;
|
||
|
||
// Only allow circuit or launch modes
|
||
if (mode != RideMode::ContinuousCircuit && mode != RideMode::ReverseInclineLaunchedShuttle
|
||
&& mode != RideMode::PoweredLaunchPasstrough)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// Must have no more than one vehicle and one station
|
||
if (NumTrains > 1 || num_stations > 1)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
bool Ride::SupportsStatus(RideStatus s) const
|
||
{
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
|
||
switch (s)
|
||
{
|
||
case RideStatus::Closed:
|
||
case RideStatus::Open:
|
||
return true;
|
||
case RideStatus::Simulating:
|
||
return (!rtd.HasFlag(RIDE_TYPE_FLAG_NO_TEST_MODE) && rtd.HasFlag(RIDE_TYPE_FLAG_HAS_TRACK));
|
||
case RideStatus::Testing:
|
||
return !rtd.HasFlag(RIDE_TYPE_FLAG_NO_TEST_MODE);
|
||
case RideStatus::Count: // Meaningless but necessary to satisfy -Wswitch
|
||
return false;
|
||
}
|
||
// Unreachable
|
||
return false;
|
||
}
|
||
|
||
#pragma region Initialisation functions
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006ACA89
|
||
*/
|
||
void RideInitAll()
|
||
{
|
||
auto& gameState = GetGameState();
|
||
std::for_each(std::begin(gameState.Rides), std::end(gameState.Rides), RideReset);
|
||
_endOfUsedRange = 0;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7A38
|
||
*/
|
||
void ResetAllRideBuildDates()
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
ride.build_date -= GetDate().GetMonthsElapsed();
|
||
}
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Construction
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Update functions
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006ABE4C
|
||
*/
|
||
void Ride::UpdateAll()
|
||
{
|
||
PROFILED_FUNCTION();
|
||
|
||
// Remove all rides if scenario editor
|
||
if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR)
|
||
{
|
||
switch (GetGameState().EditorStep)
|
||
{
|
||
case EditorStep::ObjectSelection:
|
||
case EditorStep::LandscapeEditor:
|
||
case EditorStep::InventionsListSetUp:
|
||
for (auto& ride : GetRideManager())
|
||
ride.Delete();
|
||
break;
|
||
case EditorStep::OptionsSelection:
|
||
case EditorStep::ObjectiveSelection:
|
||
case EditorStep::SaveScenario:
|
||
case EditorStep::RollercoasterDesigner:
|
||
case EditorStep::DesignsManager:
|
||
case EditorStep::Invalid:
|
||
break;
|
||
}
|
||
return;
|
||
}
|
||
|
||
WindowUpdateViewportRideMusic();
|
||
|
||
// Update rides
|
||
for (auto& ride : GetRideManager())
|
||
ride.Update();
|
||
|
||
OpenRCT2::RideAudio::UpdateMusicChannels();
|
||
}
|
||
|
||
std::unique_ptr<TrackDesign> Ride::SaveToTrackDesign(TrackDesignState& tds) const
|
||
{
|
||
if (!(lifecycle_flags & RIDE_LIFECYCLE_TESTED))
|
||
{
|
||
ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, STR_NONE, {});
|
||
return nullptr;
|
||
}
|
||
|
||
if (!RideHasRatings(*this))
|
||
{
|
||
ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, STR_NONE, {});
|
||
return nullptr;
|
||
}
|
||
|
||
auto td = std::make_unique<TrackDesign>();
|
||
auto errMessage = td->CreateTrackDesign(tds, *this);
|
||
if (!errMessage.Successful)
|
||
{
|
||
ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, errMessage.Message, {});
|
||
return nullptr;
|
||
}
|
||
if (errMessage.HasMessage())
|
||
{
|
||
ContextShowError(errMessage.Message, STR_EMPTY, {});
|
||
}
|
||
|
||
return td;
|
||
}
|
||
|
||
RideStation& Ride::GetStation(StationIndex stationIndex)
|
||
{
|
||
return stations[stationIndex.ToUnderlying()];
|
||
}
|
||
|
||
StationIndex::UnderlyingType Ride::GetStationNumber(StationIndex in) const
|
||
{
|
||
StationIndex::UnderlyingType nullStationsSeen{ 0 };
|
||
for (size_t i = 0; i < in.ToUnderlying(); i++)
|
||
{
|
||
if (stations[i].Start.IsNull())
|
||
{
|
||
nullStationsSeen++;
|
||
}
|
||
}
|
||
|
||
return in.ToUnderlying() - nullStationsSeen + 1;
|
||
}
|
||
|
||
const RideStation& Ride::GetStation(StationIndex stationIndex) const
|
||
{
|
||
return stations[stationIndex.ToUnderlying()];
|
||
}
|
||
|
||
std::array<RideStation, OpenRCT2::Limits::MaxStationsPerRide>& Ride::GetStations()
|
||
{
|
||
return stations;
|
||
}
|
||
|
||
const std::array<RideStation, OpenRCT2::Limits::MaxStationsPerRide>& Ride::GetStations() const
|
||
{
|
||
return stations;
|
||
}
|
||
|
||
StationIndex Ride::GetStationIndex(const RideStation* station) const
|
||
{
|
||
auto distance = std::distance(stations.data(), station);
|
||
Guard::Assert(distance >= 0 && distance < int32_t(std::size(stations)));
|
||
return StationIndex::FromUnderlying(distance);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006ABE73
|
||
*/
|
||
void Ride::Update()
|
||
{
|
||
if (vehicle_change_timeout != 0)
|
||
vehicle_change_timeout--;
|
||
|
||
RideMusicUpdate(*this);
|
||
|
||
// Update stations
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
for (StationIndex::UnderlyingType i = 0; i < OpenRCT2::Limits::MaxStationsPerRide; i++)
|
||
RideUpdateStation(*this, StationIndex::FromUnderlying(i));
|
||
|
||
// Update financial statistics
|
||
num_customers_timeout++;
|
||
|
||
if (num_customers_timeout >= 960)
|
||
{
|
||
// This is meant to update about every 30 seconds
|
||
num_customers_timeout = 0;
|
||
|
||
// Shift number of customers history, start of the array is the most recent one
|
||
for (int32_t i = OpenRCT2::Limits::CustomerHistorySize - 1; i > 0; i--)
|
||
{
|
||
num_customers[i] = num_customers[i - 1];
|
||
}
|
||
num_customers[0] = cur_num_customers;
|
||
|
||
cur_num_customers = 0;
|
||
window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
||
|
||
income_per_hour = CalculateIncomePerHour();
|
||
window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME;
|
||
|
||
if (upkeep_cost != kMoney64Undefined)
|
||
profit = income_per_hour - (upkeep_cost * 16);
|
||
}
|
||
|
||
// Ride specific updates
|
||
if (rtd.RideUpdate != nullptr)
|
||
rtd.RideUpdate(*this);
|
||
|
||
RideBreakdownUpdate(*this);
|
||
|
||
// Various things include news messages
|
||
if (lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_DUE_INSPECTION))
|
||
{
|
||
// Breakdown updates originally were performed when (id == (gCurrentTicks / 2) & 0xFF)
|
||
// with the increased MAX_RIDES the update is tied to the first byte of the id this allows
|
||
// for identical balance with vanilla.
|
||
const auto updatingRideByte = static_cast<uint8_t>((GetGameState().CurrentTicks / 2) & 0xFF);
|
||
if (updatingRideByte == static_cast<uint8_t>(id.ToUnderlying()))
|
||
RideBreakdownStatusUpdate(*this);
|
||
}
|
||
|
||
RideInspectionUpdate(*this);
|
||
|
||
// If ride is simulating but crashed, reset the vehicles
|
||
if (status == RideStatus::Simulating && (lifecycle_flags & RIDE_LIFECYCLE_CRASHED))
|
||
{
|
||
if (mode == RideMode::ContinuousCircuitBlockSectioned || mode == RideMode::PoweredLaunchBlockSectioned)
|
||
{
|
||
// We require this to execute right away during the simulation, always ignore network and queue.
|
||
RideSetStatusAction gameAction = RideSetStatusAction(id, RideStatus::Closed);
|
||
GameActions::ExecuteNested(&gameAction);
|
||
}
|
||
else
|
||
{
|
||
// We require this to execute right away during the simulation, always ignore network and queue.
|
||
RideSetStatusAction gameAction = RideSetStatusAction(id, RideStatus::Simulating);
|
||
GameActions::ExecuteNested(&gameAction);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC489
|
||
*/
|
||
void UpdateChairlift(Ride& ride)
|
||
{
|
||
if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK))
|
||
return;
|
||
if ((ride.lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_CRASHED))
|
||
&& ride.breakdown_reason_pending == 0)
|
||
return;
|
||
|
||
uint16_t old_chairlift_bullwheel_rotation = ride.chairlift_bullwheel_rotation >> 14;
|
||
ride.chairlift_bullwheel_rotation += ride.speed * 2048;
|
||
if (old_chairlift_bullwheel_rotation == ride.speed / 8)
|
||
return;
|
||
|
||
auto bullwheelLoc = ride.ChairliftBullwheelLocation[0].ToCoordsXYZ();
|
||
MapInvalidateTileZoom1({ bullwheelLoc, bullwheelLoc.z, bullwheelLoc.z + (4 * COORDS_Z_STEP) });
|
||
|
||
bullwheelLoc = ride.ChairliftBullwheelLocation[1].ToCoordsXYZ();
|
||
MapInvalidateTileZoom1({ bullwheelLoc, bullwheelLoc.z, bullwheelLoc.z + (4 * COORDS_Z_STEP) });
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x0069A3A2
|
||
* edi: ride (in code as bytes offset from start of rides list)
|
||
* bl: happiness
|
||
*/
|
||
void Ride::UpdateSatisfaction(const uint8_t happiness)
|
||
{
|
||
satisfaction_next += happiness;
|
||
satisfaction_time_out++;
|
||
if (satisfaction_time_out >= 20)
|
||
{
|
||
satisfaction = satisfaction_next >> 2;
|
||
satisfaction_next = 0;
|
||
satisfaction_time_out = 0;
|
||
window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x0069A3D7
|
||
* Updates the ride popularity
|
||
* edi : ride
|
||
* bl : pop_amount
|
||
* pop_amount can be zero if peep visited but did not purchase.
|
||
*/
|
||
void Ride::UpdatePopularity(const uint8_t pop_amount)
|
||
{
|
||
popularity_next += pop_amount;
|
||
popularity_time_out++;
|
||
if (popularity_time_out < 25)
|
||
return;
|
||
|
||
popularity = popularity_next;
|
||
popularity_next = 0;
|
||
popularity_time_out = 0;
|
||
window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
||
}
|
||
|
||
/** rct2: 0x0098DDB8, 0x0098DDBA */
|
||
static constexpr CoordsXY ride_spiral_slide_main_tile_offset[][4] = {
|
||
{
|
||
{ 32, 32 },
|
||
{ 0, 32 },
|
||
{ 0, 0 },
|
||
{ 32, 0 },
|
||
},
|
||
{
|
||
{ 32, 0 },
|
||
{ 0, 0 },
|
||
{ 0, -32 },
|
||
{ 32, -32 },
|
||
},
|
||
{
|
||
{ 0, 0 },
|
||
{ -32, 0 },
|
||
{ -32, -32 },
|
||
{ 0, -32 },
|
||
},
|
||
{
|
||
{ 0, 0 },
|
||
{ 0, 32 },
|
||
{ -32, 32 },
|
||
{ -32, 0 },
|
||
},
|
||
};
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC545
|
||
*/
|
||
|
||
void UpdateSpiralSlide(Ride& ride)
|
||
{
|
||
if (GetGameState().CurrentTicks & 3)
|
||
return;
|
||
if (ride.slide_in_use == 0)
|
||
return;
|
||
|
||
ride.spiral_slide_progress++;
|
||
if (ride.spiral_slide_progress >= 48)
|
||
{
|
||
ride.slide_in_use--;
|
||
|
||
auto* peep = GetEntity<Guest>(ride.slide_peep);
|
||
if (peep != nullptr)
|
||
{
|
||
auto destination = peep->GetDestination();
|
||
destination.x++;
|
||
peep->SetDestination(destination);
|
||
}
|
||
}
|
||
|
||
const uint8_t current_rotation = GetCurrentRotation();
|
||
// Invalidate something related to station start
|
||
for (int32_t i = 0; i < OpenRCT2::Limits::MaxStationsPerRide; i++)
|
||
{
|
||
if (ride.stations[i].Start.IsNull())
|
||
continue;
|
||
|
||
auto startLoc = ride.stations[i].Start;
|
||
|
||
TileElement* tileElement = RideGetStationStartTrackElement(ride, StationIndex::FromUnderlying(i));
|
||
if (tileElement == nullptr)
|
||
continue;
|
||
|
||
int32_t rotation = tileElement->GetDirection();
|
||
startLoc += ride_spiral_slide_main_tile_offset[rotation][current_rotation];
|
||
|
||
MapInvalidateTileZoom0({ startLoc, tileElement->GetBaseZ(), tileElement->GetClearanceZ() });
|
||
}
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Breakdown and inspection functions
|
||
|
||
static uint8_t _breakdownProblemProbabilities[] = {
|
||
25, // BREAKDOWN_SAFETY_CUT_OUT
|
||
12, // BREAKDOWN_RESTRAINTS_STUCK_CLOSED
|
||
10, // BREAKDOWN_RESTRAINTS_STUCK_OPEN
|
||
13, // BREAKDOWN_DOORS_STUCK_CLOSED
|
||
10, // BREAKDOWN_DOORS_STUCK_OPEN
|
||
6, // BREAKDOWN_VEHICLE_MALFUNCTION
|
||
0, // BREAKDOWN_BRAKES_FAILURE
|
||
3, // BREAKDOWN_CONTROL_FAILURE
|
||
};
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC7C2
|
||
*/
|
||
static void RideInspectionUpdate(Ride& ride)
|
||
{
|
||
if (GetGameState().CurrentTicks & 2047)
|
||
return;
|
||
if (gScreenFlags & SCREEN_FLAGS_TRACK_DESIGNER)
|
||
return;
|
||
|
||
ride.last_inspection++;
|
||
if (ride.last_inspection == 0)
|
||
ride.last_inspection--;
|
||
|
||
int32_t inspectionIntervalMinutes = RideInspectionInterval[ride.inspection_interval];
|
||
// An inspection interval of 0 minutes means the ride is set to never be inspected.
|
||
if (inspectionIntervalMinutes == 0)
|
||
{
|
||
ride.lifecycle_flags &= ~RIDE_LIFECYCLE_DUE_INSPECTION;
|
||
return;
|
||
}
|
||
|
||
if (ride.GetRideTypeDescriptor().AvailableBreakdowns == 0)
|
||
return;
|
||
|
||
if (inspectionIntervalMinutes > ride.last_inspection)
|
||
return;
|
||
|
||
if (ride.lifecycle_flags
|
||
& (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_DUE_INSPECTION
|
||
| RIDE_LIFECYCLE_CRASHED))
|
||
return;
|
||
|
||
// Inspect the first station that has an exit
|
||
ride.lifecycle_flags |= RIDE_LIFECYCLE_DUE_INSPECTION;
|
||
ride.mechanic_status = RIDE_MECHANIC_STATUS_CALLING;
|
||
|
||
auto stationIndex = RideGetFirstValidStationExit(ride);
|
||
ride.inspection_station = (!stationIndex.IsNull()) ? stationIndex : StationIndex::FromUnderlying(0);
|
||
}
|
||
|
||
static int32_t GetAgePenalty(const Ride& ride)
|
||
{
|
||
auto years = DateGetYear(ride.GetAge());
|
||
switch (years)
|
||
{
|
||
case 0:
|
||
return 0;
|
||
case 1:
|
||
return ride.unreliability_factor / 8;
|
||
case 2:
|
||
return ride.unreliability_factor / 4;
|
||
case 3:
|
||
case 4:
|
||
return ride.unreliability_factor / 2;
|
||
case 5:
|
||
case 6:
|
||
case 7:
|
||
return ride.unreliability_factor;
|
||
default:
|
||
return ride.unreliability_factor * 2;
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC622
|
||
*/
|
||
static void RideBreakdownUpdate(Ride& ride)
|
||
{
|
||
const auto currentTicks = GetGameState().CurrentTicks;
|
||
if (currentTicks & 255)
|
||
return;
|
||
|
||
if (gScreenFlags & SCREEN_FLAGS_TRACK_DESIGNER)
|
||
return;
|
||
|
||
if (ride.lifecycle_flags & (RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_CRASHED))
|
||
ride.downtime_history[0]++;
|
||
|
||
if (!(currentTicks & 8191))
|
||
{
|
||
int32_t totalDowntime = 0;
|
||
|
||
for (int32_t i = 0; i < OpenRCT2::Limits::DowntimeHistorySize; i++)
|
||
{
|
||
totalDowntime += ride.downtime_history[i];
|
||
}
|
||
|
||
ride.downtime = std::min(totalDowntime / 2, 100);
|
||
|
||
for (int32_t i = OpenRCT2::Limits::DowntimeHistorySize - 1; i > 0; i--)
|
||
{
|
||
ride.downtime_history[i] = ride.downtime_history[i - 1];
|
||
}
|
||
ride.downtime_history[0] = 0;
|
||
|
||
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAINTENANCE;
|
||
}
|
||
|
||
if (ride.lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_CRASHED))
|
||
return;
|
||
if (ride.status == RideStatus::Closed || ride.status == RideStatus::Simulating)
|
||
return;
|
||
|
||
if (!ride.CanBreakDown())
|
||
{
|
||
ride.reliability = kRideInitialReliability;
|
||
return;
|
||
}
|
||
|
||
// Calculate breakdown probability?
|
||
int32_t unreliabilityAccumulator = ride.unreliability_factor + GetAgePenalty(ride);
|
||
ride.reliability = static_cast<uint16_t>(std::max(0, (ride.reliability - unreliabilityAccumulator)));
|
||
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAINTENANCE;
|
||
|
||
// Random probability of a breakdown. Roughly this is 1 in
|
||
//
|
||
// (25000 - reliability) / 3 000 000
|
||
//
|
||
// a 0.8% chance, less the breakdown factor which accumulates as the game
|
||
// continues.
|
||
if ((ride.reliability == 0
|
||
|| static_cast<uint32_t>(ScenarioRand() & 0x2FFFFF) <= 1u + kRideInitialReliability - ride.reliability)
|
||
&& !GetGameState().Cheats.DisableAllBreakdowns)
|
||
{
|
||
int32_t breakdownReason = RideGetNewBreakdownProblem(ride);
|
||
if (breakdownReason != -1)
|
||
RidePrepareBreakdown(ride, breakdownReason);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7294
|
||
*/
|
||
static int32_t RideGetNewBreakdownProblem(const Ride& ride)
|
||
{
|
||
int32_t availableBreakdownProblems, totalProbability, randomProbability, problemBits, breakdownProblem;
|
||
|
||
// Brake failure is more likely when it's raining
|
||
_breakdownProblemProbabilities[BREAKDOWN_BRAKES_FAILURE] = ClimateIsRaining() ? 20 : 3;
|
||
|
||
if (!ride.CanBreakDown())
|
||
return -1;
|
||
|
||
availableBreakdownProblems = ride.GetRideTypeDescriptor().AvailableBreakdowns;
|
||
|
||
// Calculate the total probability range for all possible breakdown problems
|
||
totalProbability = 0;
|
||
problemBits = availableBreakdownProblems;
|
||
while (problemBits != 0)
|
||
{
|
||
breakdownProblem = UtilBitScanForward(problemBits);
|
||
problemBits &= ~(1 << breakdownProblem);
|
||
totalProbability += _breakdownProblemProbabilities[breakdownProblem];
|
||
}
|
||
if (totalProbability == 0)
|
||
return -1;
|
||
|
||
// Choose a random number within this range
|
||
randomProbability = ScenarioRand() % totalProbability;
|
||
|
||
// Find which problem range the random number lies
|
||
problemBits = availableBreakdownProblems;
|
||
do
|
||
{
|
||
breakdownProblem = UtilBitScanForward(problemBits);
|
||
problemBits &= ~(1 << breakdownProblem);
|
||
randomProbability -= _breakdownProblemProbabilities[breakdownProblem];
|
||
} while (randomProbability >= 0);
|
||
|
||
if (breakdownProblem != BREAKDOWN_BRAKES_FAILURE)
|
||
return breakdownProblem;
|
||
|
||
// Brakes failure can not happen if block brakes are used (so long as there is more than one vehicle)
|
||
// However if this is the case, brake failure should be taken out the equation, otherwise block brake
|
||
// rides have a lower probability to break down due to a random implementation reason.
|
||
if (ride.IsBlockSectioned())
|
||
if (ride.NumTrains != 1)
|
||
return -1;
|
||
|
||
// If brakes failure is disabled, also take it out of the equation (see above comment why)
|
||
if (GetGameState().Cheats.DisableBrakesFailure)
|
||
return -1;
|
||
|
||
auto monthsOld = ride.GetAge();
|
||
if (monthsOld < 16 || ride.reliability_percentage > 50)
|
||
return -1;
|
||
|
||
return BREAKDOWN_BRAKES_FAILURE;
|
||
}
|
||
|
||
bool Ride::CanBreakDown() const
|
||
{
|
||
if (GetRideTypeDescriptor().AvailableBreakdowns == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const auto* entry = GetRideEntry();
|
||
return entry != nullptr && !(entry->flags & RIDE_ENTRY_FLAG_CANNOT_BREAK_DOWN);
|
||
}
|
||
|
||
static void ChooseRandomTrainToBreakdownSafe(Ride& ride)
|
||
{
|
||
// Prevent integer division by zero in case of hacked ride.
|
||
if (ride.NumTrains == 0)
|
||
return;
|
||
|
||
ride.broken_vehicle = ScenarioRand() % ride.NumTrains;
|
||
|
||
// Prevent crash caused by accessing SPRITE_INDEX_NULL on hacked rides.
|
||
// This should probably be cleaned up on import instead.
|
||
while (ride.vehicles[ride.broken_vehicle].IsNull() && ride.broken_vehicle != 0)
|
||
{
|
||
--ride.broken_vehicle;
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7348
|
||
*/
|
||
void RidePrepareBreakdown(Ride& ride, int32_t breakdownReason)
|
||
{
|
||
StationIndex i;
|
||
Vehicle* vehicle;
|
||
|
||
if (ride.lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_CRASHED))
|
||
return;
|
||
|
||
ride.lifecycle_flags |= RIDE_LIFECYCLE_BREAKDOWN_PENDING;
|
||
|
||
ride.breakdown_reason_pending = breakdownReason;
|
||
ride.breakdown_sound_modifier = 0;
|
||
ride.not_fixed_timeout = 0;
|
||
ride.inspection_station = StationIndex::FromUnderlying(0); // ensure set to something.
|
||
|
||
switch (breakdownReason)
|
||
{
|
||
case BREAKDOWN_SAFETY_CUT_OUT:
|
||
case BREAKDOWN_CONTROL_FAILURE:
|
||
// Inspect first station with an exit
|
||
i = RideGetFirstValidStationExit(ride);
|
||
if (!i.IsNull())
|
||
{
|
||
ride.inspection_station = i;
|
||
}
|
||
|
||
break;
|
||
case BREAKDOWN_RESTRAINTS_STUCK_CLOSED:
|
||
case BREAKDOWN_RESTRAINTS_STUCK_OPEN:
|
||
case BREAKDOWN_DOORS_STUCK_CLOSED:
|
||
case BREAKDOWN_DOORS_STUCK_OPEN:
|
||
// Choose a random train and car
|
||
ChooseRandomTrainToBreakdownSafe(ride);
|
||
if (ride.num_cars_per_train != 0)
|
||
{
|
||
ride.broken_car = ScenarioRand() % ride.num_cars_per_train;
|
||
|
||
// Set flag on broken car
|
||
vehicle = GetEntity<Vehicle>(ride.vehicles[ride.broken_vehicle]);
|
||
if (vehicle != nullptr)
|
||
{
|
||
vehicle = vehicle->GetCar(ride.broken_car);
|
||
}
|
||
if (vehicle != nullptr)
|
||
{
|
||
vehicle->SetFlag(VehicleFlags::CarIsBroken);
|
||
}
|
||
}
|
||
break;
|
||
case BREAKDOWN_VEHICLE_MALFUNCTION:
|
||
// Choose a random train
|
||
ChooseRandomTrainToBreakdownSafe(ride);
|
||
ride.broken_car = 0;
|
||
|
||
// Set flag on broken train, first car
|
||
vehicle = GetEntity<Vehicle>(ride.vehicles[ride.broken_vehicle]);
|
||
if (vehicle != nullptr)
|
||
{
|
||
vehicle->SetFlag(VehicleFlags::TrainIsBroken);
|
||
}
|
||
break;
|
||
case BREAKDOWN_BRAKES_FAILURE:
|
||
// Original code generates a random number but does not use it
|
||
// Unsure if this was supposed to choose a random station (or random station with an exit)
|
||
i = RideGetFirstValidStationExit(ride);
|
||
if (!i.IsNull())
|
||
{
|
||
ride.inspection_station = i;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B74FA
|
||
*/
|
||
void RideBreakdownAddNewsItem(const Ride& ride)
|
||
{
|
||
if (gConfigNotifications.RideBrokenDown)
|
||
{
|
||
Formatter ft;
|
||
ride.FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_RIDE_IS_BROKEN_DOWN, ride.id.ToUnderlying(), ft);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B75C8
|
||
*/
|
||
static void RideBreakdownStatusUpdate(Ride& ride)
|
||
{
|
||
// Warn player if ride hasn't been fixed for ages
|
||
if (ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
|
||
{
|
||
ride.not_fixed_timeout++;
|
||
// When there has been a full 255 timeout ticks this
|
||
// will force timeout ticks to keep issuing news every
|
||
// 16 ticks. Note there is no reason to do this.
|
||
if (ride.not_fixed_timeout == 0)
|
||
ride.not_fixed_timeout -= 16;
|
||
|
||
if (!(ride.not_fixed_timeout & 15) && ride.mechanic_status != RIDE_MECHANIC_STATUS_FIXING
|
||
&& ride.mechanic_status != RIDE_MECHANIC_STATUS_HAS_FIXED_STATION_BRAKES)
|
||
{
|
||
if (gConfigNotifications.RideWarnings)
|
||
{
|
||
Formatter ft;
|
||
ride.FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_RIDE_IS_STILL_NOT_FIXED, ride.id.ToUnderlying(), ft);
|
||
}
|
||
}
|
||
}
|
||
|
||
RideMechanicStatusUpdate(ride, ride.mechanic_status);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B762F
|
||
*/
|
||
static void RideMechanicStatusUpdate(Ride& ride, int32_t mechanicStatus)
|
||
{
|
||
// Turn a pending breakdown into a breakdown.
|
||
if ((mechanicStatus == RIDE_MECHANIC_STATUS_UNDEFINED || mechanicStatus == RIDE_MECHANIC_STATUS_CALLING
|
||
|| mechanicStatus == RIDE_MECHANIC_STATUS_HEADING)
|
||
&& (ride.lifecycle_flags & RIDE_LIFECYCLE_BREAKDOWN_PENDING) && !(ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN))
|
||
{
|
||
auto breakdownReason = ride.breakdown_reason_pending;
|
||
if (breakdownReason == BREAKDOWN_SAFETY_CUT_OUT || breakdownReason == BREAKDOWN_BRAKES_FAILURE
|
||
|| breakdownReason == BREAKDOWN_CONTROL_FAILURE)
|
||
{
|
||
ride.lifecycle_flags |= RIDE_LIFECYCLE_BROKEN_DOWN;
|
||
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAINTENANCE | RIDE_INVALIDATE_RIDE_LIST
|
||
| RIDE_INVALIDATE_RIDE_MAIN;
|
||
ride.breakdown_reason = breakdownReason;
|
||
RideBreakdownAddNewsItem(ride);
|
||
}
|
||
}
|
||
switch (mechanicStatus)
|
||
{
|
||
case RIDE_MECHANIC_STATUS_UNDEFINED:
|
||
if (ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
|
||
{
|
||
ride.mechanic_status = RIDE_MECHANIC_STATUS_CALLING;
|
||
}
|
||
break;
|
||
case RIDE_MECHANIC_STATUS_CALLING:
|
||
if (ride.GetRideTypeDescriptor().AvailableBreakdowns == 0)
|
||
{
|
||
ride.lifecycle_flags &= ~(
|
||
RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_DUE_INSPECTION);
|
||
break;
|
||
}
|
||
|
||
RideCallClosestMechanic(ride);
|
||
break;
|
||
case RIDE_MECHANIC_STATUS_HEADING:
|
||
{
|
||
auto mechanic = RideGetMechanic(ride);
|
||
bool rideNeedsRepair = (ride.lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN));
|
||
if (mechanic == nullptr
|
||
|| (mechanic->State != PeepState::HeadingToInspection && mechanic->State != PeepState::Answering)
|
||
|| mechanic->CurrentRide != ride.id)
|
||
{
|
||
ride.mechanic_status = RIDE_MECHANIC_STATUS_CALLING;
|
||
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAINTENANCE;
|
||
RideMechanicStatusUpdate(ride, RIDE_MECHANIC_STATUS_CALLING);
|
||
}
|
||
// if the ride is broken down, but a mechanic was heading for an inspection, update orders to fix
|
||
else if (rideNeedsRepair && mechanic->State == PeepState::HeadingToInspection)
|
||
{
|
||
// updates orders for mechanic already heading to inspect ride
|
||
// forInspection == false means start repair (goes to PeepState::Answering)
|
||
RideCallMechanic(ride, mechanic, false);
|
||
}
|
||
break;
|
||
}
|
||
case RIDE_MECHANIC_STATUS_FIXING:
|
||
{
|
||
auto mechanic = RideGetMechanic(ride);
|
||
if (mechanic == nullptr
|
||
|| (mechanic->State != PeepState::HeadingToInspection && mechanic->State != PeepState::Fixing
|
||
&& mechanic->State != PeepState::Inspecting && mechanic->State != PeepState::Answering))
|
||
{
|
||
ride.mechanic_status = RIDE_MECHANIC_STATUS_CALLING;
|
||
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAINTENANCE;
|
||
RideMechanicStatusUpdate(ride, RIDE_MECHANIC_STATUS_CALLING);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B796C
|
||
*/
|
||
static void RideCallMechanic(Ride& ride, Peep* mechanic, int32_t forInspection)
|
||
{
|
||
mechanic->SetState(forInspection ? PeepState::HeadingToInspection : PeepState::Answering);
|
||
mechanic->SubState = 0;
|
||
ride.mechanic_status = RIDE_MECHANIC_STATUS_HEADING;
|
||
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAINTENANCE;
|
||
ride.mechanic = mechanic->Id;
|
||
mechanic->CurrentRide = ride.id;
|
||
mechanic->CurrentRideStation = ride.inspection_station;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B76AB
|
||
*/
|
||
static void RideCallClosestMechanic(Ride& ride)
|
||
{
|
||
auto forInspection = (ride.lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN)) == 0;
|
||
auto mechanic = RideFindClosestMechanic(ride, forInspection);
|
||
if (mechanic != nullptr)
|
||
RideCallMechanic(ride, mechanic, forInspection);
|
||
}
|
||
|
||
Staff* RideFindClosestMechanic(const Ride& ride, int32_t forInspection)
|
||
{
|
||
// Get either exit position or entrance position if there is no exit
|
||
auto& station = ride.GetStation(ride.inspection_station);
|
||
TileCoordsXYZD location = station.Exit;
|
||
if (location.IsNull())
|
||
{
|
||
location = station.Entrance;
|
||
if (station.Entrance.IsNull())
|
||
return nullptr;
|
||
}
|
||
|
||
// Get station start track element and position
|
||
auto mapLocation = location.ToCoordsXYZ();
|
||
TileElement* tileElement = RideGetStationExitElement(mapLocation);
|
||
if (tileElement == nullptr)
|
||
return nullptr;
|
||
|
||
// Set x,y to centre of the station exit for the mechanic search.
|
||
auto centreMapLocation = mapLocation.ToTileCentre();
|
||
|
||
return FindClosestMechanic(centreMapLocation, forInspection);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B774B (forInspection = 0)
|
||
* rct2: 0x006B78C3 (forInspection = 1)
|
||
*/
|
||
Staff* FindClosestMechanic(const CoordsXY& entrancePosition, int32_t forInspection)
|
||
{
|
||
Staff* closestMechanic = nullptr;
|
||
uint32_t closestDistance = std::numeric_limits<uint32_t>::max();
|
||
|
||
for (auto peep : EntityList<Staff>())
|
||
{
|
||
if (!peep->IsMechanic())
|
||
continue;
|
||
|
||
if (!forInspection)
|
||
{
|
||
if (peep->State == PeepState::HeadingToInspection)
|
||
{
|
||
if (peep->SubState >= 4)
|
||
continue;
|
||
}
|
||
else if (peep->State != PeepState::Patrolling)
|
||
continue;
|
||
|
||
if (!(peep->StaffOrders & STAFF_ORDERS_FIX_RIDES))
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
if (peep->State != PeepState::Patrolling || !(peep->StaffOrders & STAFF_ORDERS_INSPECT_RIDES))
|
||
continue;
|
||
}
|
||
|
||
auto location = entrancePosition.ToTileStart();
|
||
if (MapIsLocationInPark(location))
|
||
if (!peep->IsLocationInPatrol(location))
|
||
continue;
|
||
|
||
if (peep->x == LOCATION_NULL)
|
||
continue;
|
||
|
||
// Manhattan distance
|
||
uint32_t distance = std::abs(peep->x - entrancePosition.x) + std::abs(peep->y - entrancePosition.y);
|
||
if (distance < closestDistance)
|
||
{
|
||
closestDistance = distance;
|
||
closestMechanic = peep;
|
||
}
|
||
}
|
||
|
||
return closestMechanic;
|
||
}
|
||
|
||
Staff* RideGetMechanic(const Ride& ride)
|
||
{
|
||
auto staff = GetEntity<Staff>(ride.mechanic);
|
||
if (staff != nullptr && staff->IsMechanic())
|
||
{
|
||
return staff;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
Staff* RideGetAssignedMechanic(const Ride& ride)
|
||
{
|
||
if (ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
|
||
{
|
||
if (ride.mechanic_status == RIDE_MECHANIC_STATUS_HEADING || ride.mechanic_status == RIDE_MECHANIC_STATUS_FIXING
|
||
|| ride.mechanic_status == RIDE_MECHANIC_STATUS_HAS_FIXED_STATION_BRAKES)
|
||
{
|
||
return RideGetMechanic(ride);
|
||
}
|
||
}
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Music functions
|
||
|
||
/**
|
||
*
|
||
* Calculates the sample rate for ride music.
|
||
*/
|
||
static int32_t RideMusicSampleRate(const Ride& ride)
|
||
{
|
||
int32_t sampleRate = 22050;
|
||
|
||
// Alter sample rate for a power cut effect
|
||
if (ride.lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN))
|
||
{
|
||
sampleRate = ride.breakdown_sound_modifier * 70;
|
||
if (ride.breakdown_reason_pending != BREAKDOWN_CONTROL_FAILURE)
|
||
sampleRate *= -1;
|
||
sampleRate += 22050;
|
||
}
|
||
|
||
return sampleRate;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Ride music slows down upon breaking. If it's completely broken, no music should play.
|
||
*/
|
||
static bool RideMusicBreakdownEffect(Ride& ride)
|
||
{
|
||
// Oscillate parameters for a power cut effect when breaking down
|
||
if (ride.lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN))
|
||
{
|
||
if (ride.breakdown_reason_pending == BREAKDOWN_CONTROL_FAILURE)
|
||
{
|
||
if (!(GetGameState().CurrentTicks & 7))
|
||
if (ride.breakdown_sound_modifier != 255)
|
||
ride.breakdown_sound_modifier++;
|
||
}
|
||
else
|
||
{
|
||
if ((ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN) || ride.breakdown_reason_pending == BREAKDOWN_BRAKES_FAILURE
|
||
|| ride.breakdown_reason_pending == BREAKDOWN_CONTROL_FAILURE)
|
||
{
|
||
if (ride.breakdown_sound_modifier != 255)
|
||
ride.breakdown_sound_modifier++;
|
||
}
|
||
|
||
if (ride.breakdown_sound_modifier == 255)
|
||
{
|
||
ride.music_tune_id = TUNE_ID_NULL;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Circus music is a sound effect, rather than music. Needs separate processing.
|
||
*/
|
||
void CircusMusicUpdate(Ride& ride)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(ride.vehicles[0]);
|
||
if (vehicle == nullptr || vehicle->status != Vehicle::Status::DoingCircusShow)
|
||
{
|
||
ride.music_position = 0;
|
||
ride.music_tune_id = TUNE_ID_NULL;
|
||
return;
|
||
}
|
||
|
||
if (RideMusicBreakdownEffect(ride))
|
||
{
|
||
return;
|
||
}
|
||
|
||
CoordsXYZ rideCoords = ride.GetStation().GetStart().ToTileCentre();
|
||
|
||
const auto sampleRate = RideMusicSampleRate(ride);
|
||
|
||
OpenRCT2::RideAudio::UpdateMusicInstance(ride, rideCoords, sampleRate);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006ABE85
|
||
*/
|
||
void DefaultMusicUpdate(Ride& ride)
|
||
{
|
||
if (ride.status != RideStatus::Open || !(ride.lifecycle_flags & RIDE_LIFECYCLE_MUSIC))
|
||
{
|
||
ride.music_tune_id = TUNE_ID_NULL;
|
||
return;
|
||
}
|
||
|
||
if (RideMusicBreakdownEffect(ride))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Select random tune from available tunes for a music style (of course only merry-go-rounds have more than one tune)
|
||
if (ride.music_tune_id == TUNE_ID_NULL)
|
||
{
|
||
auto& objManager = GetContext()->GetObjectManager();
|
||
auto musicObj = static_cast<MusicObject*>(objManager.GetLoadedObject(ObjectType::Music, ride.music));
|
||
if (musicObj != nullptr)
|
||
{
|
||
auto numTracks = musicObj->GetTrackCount();
|
||
ride.music_tune_id = static_cast<uint8_t>(UtilRand() % numTracks);
|
||
ride.music_position = 0;
|
||
}
|
||
return;
|
||
}
|
||
|
||
CoordsXYZ rideCoords = ride.GetStation().GetStart().ToTileCentre();
|
||
|
||
int32_t sampleRate = RideMusicSampleRate(ride);
|
||
|
||
OpenRCT2::RideAudio::UpdateMusicInstance(ride, rideCoords, sampleRate);
|
||
}
|
||
|
||
static void RideMusicUpdate(Ride& ride)
|
||
{
|
||
const auto& rtd = ride.GetRideTypeDescriptor();
|
||
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_MUSIC_ON_DEFAULT) && !rtd.HasFlag(RIDE_TYPE_FLAG_ALLOW_MUSIC))
|
||
return;
|
||
rtd.MusicUpdateFunction(ride);
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Measurement functions
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B64F2
|
||
*/
|
||
static void RideMeasurementUpdate(Ride& ride, RideMeasurement& measurement)
|
||
{
|
||
if (measurement.vehicle_index >= std::size(ride.vehicles))
|
||
return;
|
||
|
||
auto vehicle = GetEntity<Vehicle>(ride.vehicles[measurement.vehicle_index]);
|
||
if (vehicle == nullptr)
|
||
return;
|
||
|
||
if (measurement.flags & RIDE_MEASUREMENT_FLAG_UNLOADING)
|
||
{
|
||
if (vehicle->status != Vehicle::Status::Departing && vehicle->status != Vehicle::Status::TravellingCableLift)
|
||
return;
|
||
|
||
measurement.flags &= ~RIDE_MEASUREMENT_FLAG_UNLOADING;
|
||
if (measurement.current_station == vehicle->current_station)
|
||
measurement.current_item = 0;
|
||
}
|
||
|
||
if (vehicle->status == Vehicle::Status::UnloadingPassengers)
|
||
{
|
||
measurement.flags |= RIDE_MEASUREMENT_FLAG_UNLOADING;
|
||
return;
|
||
}
|
||
|
||
auto trackType = vehicle->GetTrackType();
|
||
if (trackType == TrackElemType::BlockBrakes || trackType == TrackElemType::CableLiftHill
|
||
|| trackType == TrackElemType::Up25ToFlat || trackType == TrackElemType::Up60ToFlat
|
||
|| trackType == TrackElemType::DiagUp25ToFlat || trackType == TrackElemType::DiagUp60ToFlat
|
||
|| trackType == TrackElemType::DiagBlockBrakes)
|
||
if (vehicle->velocity == 0)
|
||
return;
|
||
|
||
if (measurement.current_item >= RideMeasurement::MAX_ITEMS)
|
||
return;
|
||
|
||
const auto currentTicks = GetGameState().CurrentTicks;
|
||
|
||
if (measurement.flags & RIDE_MEASUREMENT_FLAG_G_FORCES)
|
||
{
|
||
auto gForces = vehicle->GetGForces();
|
||
gForces.VerticalG = std::clamp(gForces.VerticalG / 8, -127, 127);
|
||
gForces.LateralG = std::clamp(gForces.LateralG / 8, -127, 127);
|
||
|
||
if (currentTicks & 1)
|
||
{
|
||
gForces.VerticalG = (gForces.VerticalG + measurement.vertical[measurement.current_item]) / 2;
|
||
gForces.LateralG = (gForces.LateralG + measurement.lateral[measurement.current_item]) / 2;
|
||
}
|
||
|
||
measurement.vertical[measurement.current_item] = gForces.VerticalG & 0xFF;
|
||
measurement.lateral[measurement.current_item] = gForces.LateralG & 0xFF;
|
||
}
|
||
|
||
auto velocity = std::min(std::abs((vehicle->velocity * 5) >> 16), 255);
|
||
auto altitude = std::min(vehicle->z / 8, 255);
|
||
|
||
if (currentTicks & 1)
|
||
{
|
||
velocity = (velocity + measurement.velocity[measurement.current_item]) / 2;
|
||
altitude = (altitude + measurement.altitude[measurement.current_item]) / 2;
|
||
}
|
||
|
||
measurement.velocity[measurement.current_item] = velocity & 0xFF;
|
||
measurement.altitude[measurement.current_item] = altitude & 0xFF;
|
||
|
||
if (currentTicks & 1)
|
||
{
|
||
measurement.current_item++;
|
||
measurement.num_items = std::max(measurement.num_items, measurement.current_item);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B6456
|
||
*/
|
||
void RideMeasurementsUpdate()
|
||
{
|
||
PROFILED_FUNCTION();
|
||
|
||
if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR)
|
||
return;
|
||
|
||
// For each ride measurement
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
auto measurement = ride.measurement.get();
|
||
if (measurement != nullptr && (ride.lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK) && ride.status != RideStatus::Simulating)
|
||
{
|
||
if (measurement->flags & RIDE_MEASUREMENT_FLAG_RUNNING)
|
||
{
|
||
RideMeasurementUpdate(ride, *measurement);
|
||
}
|
||
else
|
||
{
|
||
// For each vehicle
|
||
for (int32_t j = 0; j < ride.NumTrains; j++)
|
||
{
|
||
auto vehicleSpriteIdx = ride.vehicles[j];
|
||
auto vehicle = GetEntity<Vehicle>(vehicleSpriteIdx);
|
||
if (vehicle != nullptr)
|
||
{
|
||
if (vehicle->status == Vehicle::Status::Departing
|
||
|| vehicle->status == Vehicle::Status::TravellingCableLift)
|
||
{
|
||
measurement->vehicle_index = j;
|
||
measurement->current_station = vehicle->current_station;
|
||
measurement->flags |= RIDE_MEASUREMENT_FLAG_RUNNING;
|
||
measurement->flags &= ~RIDE_MEASUREMENT_FLAG_UNLOADING;
|
||
RideMeasurementUpdate(ride, *measurement);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* If there are more than the threshold of allowed ride measurements, free the non-LRU one.
|
||
*/
|
||
static void RideFreeOldMeasurements()
|
||
{
|
||
size_t numRideMeasurements;
|
||
do
|
||
{
|
||
Ride* lruRide{};
|
||
numRideMeasurements = 0;
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.measurement != nullptr)
|
||
{
|
||
if (lruRide == nullptr || ride.measurement->last_use_tick > lruRide->measurement->last_use_tick)
|
||
{
|
||
lruRide = &ride;
|
||
}
|
||
numRideMeasurements++;
|
||
}
|
||
}
|
||
if (numRideMeasurements > kMaxRideMeasurements && lruRide != nullptr)
|
||
{
|
||
lruRide->measurement = {};
|
||
numRideMeasurements--;
|
||
}
|
||
} while (numRideMeasurements > kMaxRideMeasurements);
|
||
}
|
||
|
||
std::pair<RideMeasurement*, OpenRCT2String> Ride::GetMeasurement()
|
||
{
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
|
||
// Check if ride type supports data logging
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_HAS_DATA_LOGGING))
|
||
{
|
||
return { nullptr, { STR_DATA_LOGGING_NOT_AVAILABLE_FOR_THIS_TYPE_OF_RIDE, {} } };
|
||
}
|
||
|
||
// Check if a measurement already exists for this ride
|
||
if (measurement == nullptr)
|
||
{
|
||
measurement = std::make_unique<RideMeasurement>();
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_HAS_G_FORCES))
|
||
{
|
||
measurement->flags |= RIDE_MEASUREMENT_FLAG_G_FORCES;
|
||
}
|
||
RideFreeOldMeasurements();
|
||
assert(measurement != nullptr);
|
||
}
|
||
|
||
measurement->last_use_tick = GetGameState().CurrentTicks;
|
||
if (measurement->flags & 1)
|
||
{
|
||
return { measurement.get(), { STR_EMPTY, {} } };
|
||
}
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<StringId>(GetRideComponentName(rtd.NameConvention.vehicle).singular);
|
||
ft.Add<StringId>(GetRideComponentName(rtd.NameConvention.station).singular);
|
||
return { nullptr, { STR_DATA_LOGGING_WILL_START_WHEN_NEXT_LEAVES, ft } };
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Colour functions
|
||
|
||
VehicleColour RideGetVehicleColour(const Ride& ride, int32_t vehicleIndex)
|
||
{
|
||
// Prevent indexing array out of bounds
|
||
vehicleIndex = std::min<int32_t>(vehicleIndex, static_cast<int32_t>(std::size(ride.vehicle_colours)));
|
||
return ride.vehicle_colours[vehicleIndex];
|
||
}
|
||
|
||
static bool RideTypeVehicleColourExists(ObjectEntryIndex subType, const VehicleColour& vehicleColour)
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.subtype != subType)
|
||
continue;
|
||
if (ride.vehicle_colours[0].Body != vehicleColour.Body)
|
||
continue;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
int32_t RideGetUnusedPresetVehicleColour(ObjectEntryIndex subType)
|
||
{
|
||
const auto* rideEntry = GetRideEntryByIndex(subType);
|
||
if (rideEntry == nullptr)
|
||
return 0;
|
||
|
||
const auto* colourPresets = rideEntry->vehicle_preset_list;
|
||
if (colourPresets == nullptr || colourPresets->count == 0)
|
||
return 0;
|
||
if (colourPresets->count == 255)
|
||
return 255;
|
||
|
||
// Find all the presets that haven't yet been used in the park for this ride type
|
||
std::vector<uint8_t> unused;
|
||
unused.reserve(colourPresets->count);
|
||
for (uint8_t i = 0; i < colourPresets->count; i++)
|
||
{
|
||
const auto& preset = colourPresets->list[i];
|
||
if (!RideTypeVehicleColourExists(subType, preset))
|
||
{
|
||
unused.push_back(i);
|
||
}
|
||
}
|
||
|
||
// If all presets have been used, just go with a random preset
|
||
if (unused.size() == 0)
|
||
return UtilRand() % colourPresets->count;
|
||
|
||
// Choose a random preset from the list of unused presets
|
||
auto unusedIndex = UtilRand() % unused.size();
|
||
return unused[unusedIndex];
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DE52C
|
||
*/
|
||
void RideSetVehicleColoursToRandomPreset(Ride& ride, uint8_t preset_index)
|
||
{
|
||
const auto* rideEntry = GetRideEntryByIndex(ride.subtype);
|
||
const auto* presetList = rideEntry->vehicle_preset_list;
|
||
|
||
if (presetList->count != 0 && presetList->count != 255)
|
||
{
|
||
assert(preset_index < presetList->count);
|
||
|
||
ride.colour_scheme_type = RIDE_COLOUR_SCHEME_MODE_ALL_SAME;
|
||
ride.vehicle_colours[0] = presetList->list[preset_index];
|
||
}
|
||
else
|
||
{
|
||
ride.colour_scheme_type = RIDE_COLOUR_SCHEME_MODE_DIFFERENT_PER_TRAIN;
|
||
for (uint32_t i = 0; i < presetList->count; i++)
|
||
{
|
||
const auto index = i % 32U;
|
||
ride.vehicle_colours[i] = presetList->list[index];
|
||
}
|
||
}
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Reachability
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7A5E
|
||
*/
|
||
void RideCheckAllReachable()
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.connected_message_throttle != 0)
|
||
ride.connected_message_throttle--;
|
||
|
||
if (ride.status != RideStatus::Open || ride.connected_message_throttle != 0)
|
||
continue;
|
||
|
||
if (ride.GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP_OR_FACILITY))
|
||
RideShopConnected(ride);
|
||
else
|
||
RideEntranceExitConnected(ride);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7C59
|
||
* @return true if the coordinate is reachable or has no entrance, false otherwise
|
||
*/
|
||
static bool RideEntranceExitIsReachable(const TileCoordsXYZD& coordinates)
|
||
{
|
||
if (coordinates.IsNull())
|
||
return true;
|
||
|
||
TileCoordsXYZ loc{ coordinates.x, coordinates.y, coordinates.z };
|
||
loc -= TileDirectionDelta[coordinates.direction];
|
||
|
||
return MapCoordIsConnected(loc, coordinates.direction);
|
||
}
|
||
|
||
static void RideEntranceExitConnected(Ride& ride)
|
||
{
|
||
for (auto& station : ride.GetStations())
|
||
{
|
||
auto station_start = station.Start;
|
||
auto entrance = station.Entrance;
|
||
auto exit = station.Exit;
|
||
|
||
if (station_start.IsNull())
|
||
continue;
|
||
|
||
if (!entrance.IsNull() && !RideEntranceExitIsReachable(entrance))
|
||
{
|
||
// name of ride is parameter of the format string
|
||
Formatter ft;
|
||
ride.FormatNameTo(ft);
|
||
if (gConfigNotifications.RideWarnings)
|
||
{
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_ENTRANCE_NOT_CONNECTED, ride.id.ToUnderlying(), ft);
|
||
}
|
||
ride.connected_message_throttle = 3;
|
||
}
|
||
|
||
if (!exit.IsNull() && !RideEntranceExitIsReachable(exit))
|
||
{
|
||
// name of ride is parameter of the format string
|
||
Formatter ft;
|
||
ride.FormatNameTo(ft);
|
||
if (gConfigNotifications.RideWarnings)
|
||
{
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_EXIT_NOT_CONNECTED, ride.id.ToUnderlying(), ft);
|
||
}
|
||
ride.connected_message_throttle = 3;
|
||
}
|
||
}
|
||
}
|
||
|
||
static void RideShopConnected(const Ride& ride)
|
||
{
|
||
auto shopLoc = TileCoordsXY(ride.GetStation().Start);
|
||
if (shopLoc.IsNull())
|
||
return;
|
||
|
||
TrackElement* trackElement = nullptr;
|
||
TileElement* tileElement = MapGetFirstElementAt(shopLoc);
|
||
do
|
||
{
|
||
if (tileElement == nullptr)
|
||
break;
|
||
if (tileElement->GetType() == TileElementType::Track && tileElement->AsTrack()->GetRideIndex() == ride.id)
|
||
{
|
||
trackElement = tileElement->AsTrack();
|
||
break;
|
||
}
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
|
||
if (trackElement == nullptr)
|
||
return;
|
||
|
||
auto track_type = trackElement->GetTrackType();
|
||
auto ride2 = GetRide(trackElement->GetRideIndex());
|
||
if (ride2 == nullptr)
|
||
return;
|
||
|
||
const auto& ted = GetTrackElementDescriptor(track_type);
|
||
uint8_t entrance_directions = std::get<0>(ted.SequenceProperties) & 0xF;
|
||
uint8_t tile_direction = trackElement->GetDirection();
|
||
entrance_directions = Numerics::rol4(entrance_directions, tile_direction);
|
||
|
||
// Now each bit in entrance_directions stands for an entrance direction to check
|
||
if (entrance_directions == 0)
|
||
return;
|
||
|
||
for (auto count = 0; entrance_directions != 0; count++)
|
||
{
|
||
if (!(entrance_directions & 1))
|
||
{
|
||
entrance_directions >>= 1;
|
||
continue;
|
||
}
|
||
entrance_directions >>= 1;
|
||
|
||
// Flip direction north<->south, east<->west
|
||
uint8_t face_direction = DirectionReverse(count);
|
||
|
||
int32_t y2 = shopLoc.y - TileDirectionDelta[face_direction].y;
|
||
int32_t x2 = shopLoc.x - TileDirectionDelta[face_direction].x;
|
||
|
||
if (MapCoordIsConnected({ x2, y2, tileElement->BaseHeight }, face_direction))
|
||
return;
|
||
}
|
||
|
||
// Name of ride is parameter of the format string
|
||
if (gConfigNotifications.RideWarnings)
|
||
{
|
||
Formatter ft;
|
||
ride2->FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_ENTRANCE_NOT_CONNECTED, ride2->id.ToUnderlying(), ft);
|
||
}
|
||
|
||
ride2->connected_message_throttle = 3;
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Interface
|
||
|
||
static void RideTrackSetMapTooltip(const TrackElement& trackElement)
|
||
{
|
||
auto rideIndex = trackElement.GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride != nullptr)
|
||
{
|
||
auto ft = Formatter();
|
||
ft.Add<StringId>(STR_RIDE_MAP_TIP);
|
||
ride->FormatNameTo(ft);
|
||
ride->FormatStatusTo(ft);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
ContextBroadcastIntent(&intent);
|
||
}
|
||
}
|
||
|
||
static void RideQueueBannerSetMapTooltip(const PathElement& pathElement)
|
||
{
|
||
auto rideIndex = pathElement.GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return;
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<StringId>(STR_RIDE_MAP_TIP);
|
||
ride->FormatNameTo(ft);
|
||
ride->FormatStatusTo(ft);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
ContextBroadcastIntent(&intent);
|
||
}
|
||
|
||
static void RideStationSetMapTooltip(const TrackElement& trackElement)
|
||
{
|
||
auto rideIndex = trackElement.GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return;
|
||
|
||
const auto stationIndex = trackElement.GetStationIndex();
|
||
const auto stationNumber = ride->GetStationNumber(stationIndex);
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<StringId>(STR_RIDE_MAP_TIP);
|
||
ft.Add<StringId>(ride->num_stations <= 1 ? STR_RIDE_STATION : STR_RIDE_STATION_X);
|
||
ride->FormatNameTo(ft);
|
||
ft.Add<StringId>(GetRideComponentName(ride->GetRideTypeDescriptor().NameConvention.station).capitalised);
|
||
ft.Add<uint16_t>(stationNumber);
|
||
ride->FormatStatusTo(ft);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
ContextBroadcastIntent(&intent);
|
||
}
|
||
|
||
static void RideEntranceSetMapTooltip(const EntranceElement& entranceElement)
|
||
{
|
||
auto rideIndex = entranceElement.GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return;
|
||
|
||
if (entranceElement.GetEntranceType() == ENTRANCE_TYPE_RIDE_ENTRANCE)
|
||
{
|
||
// Get the queue length
|
||
int32_t queueLength = 0;
|
||
const auto stationIndex = entranceElement.GetStationIndex();
|
||
if (!ride->GetStation(stationIndex).Entrance.IsNull())
|
||
{
|
||
queueLength = ride->GetStation(stationIndex).QueueLength;
|
||
}
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<StringId>(STR_RIDE_MAP_TIP);
|
||
ft.Add<StringId>(ride->num_stations <= 1 ? STR_RIDE_ENTRANCE : STR_RIDE_STATION_X_ENTRANCE);
|
||
ride->FormatNameTo(ft);
|
||
|
||
// String IDs have an extra pop16 for some reason
|
||
ft.Increment(sizeof(uint16_t));
|
||
|
||
const auto stationNumber = ride->GetStationNumber(stationIndex);
|
||
ft.Add<uint16_t>(stationNumber);
|
||
|
||
switch (queueLength)
|
||
{
|
||
case 0:
|
||
ft.Add<StringId>(STR_QUEUE_EMPTY);
|
||
break;
|
||
case 1:
|
||
ft.Add<StringId>(STR_QUEUE_ONE_PERSON);
|
||
break;
|
||
default:
|
||
ft.Add<StringId>(STR_QUEUE_PEOPLE);
|
||
break;
|
||
}
|
||
ft.Add<uint16_t>(queueLength);
|
||
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
ContextBroadcastIntent(&intent);
|
||
}
|
||
else
|
||
{
|
||
auto ft = Formatter();
|
||
ft.Add<StringId>(ride->num_stations <= 1 ? STR_RIDE_EXIT : STR_RIDE_STATION_X_EXIT);
|
||
ride->FormatNameTo(ft);
|
||
|
||
// String IDs have an extra pop16 for some reason
|
||
ft.Increment(sizeof(uint16_t));
|
||
|
||
const auto stationIndex = entranceElement.GetStationIndex();
|
||
const auto stationNumber = ride->GetStationNumber(stationIndex);
|
||
ft.Add<uint16_t>(stationNumber);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
ContextBroadcastIntent(&intent);
|
||
}
|
||
}
|
||
|
||
void RideSetMapTooltip(const TileElement& tileElement)
|
||
{
|
||
if (tileElement.GetType() == TileElementType::Entrance)
|
||
{
|
||
RideEntranceSetMapTooltip(*tileElement.AsEntrance());
|
||
}
|
||
else if (tileElement.GetType() == TileElementType::Track)
|
||
{
|
||
const auto* trackElement = tileElement.AsTrack();
|
||
if (trackElement->IsStation())
|
||
{
|
||
RideStationSetMapTooltip(*trackElement);
|
||
}
|
||
else
|
||
{
|
||
RideTrackSetMapTooltip(*trackElement);
|
||
}
|
||
}
|
||
else if (tileElement.GetType() == TileElementType::Path)
|
||
{
|
||
RideQueueBannerSetMapTooltip(*tileElement.AsPath());
|
||
}
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4CC1
|
||
*/
|
||
static ResultWithMessage RideModeCheckValidStationNumbers(const Ride& ride)
|
||
{
|
||
uint16_t numStations = 0;
|
||
for (const auto& station : ride.GetStations())
|
||
{
|
||
if (!station.Start.IsNull())
|
||
{
|
||
numStations++;
|
||
}
|
||
}
|
||
|
||
switch (ride.mode)
|
||
{
|
||
case RideMode::ReverseInclineLaunchedShuttle:
|
||
case RideMode::PoweredLaunchPasstrough:
|
||
case RideMode::PoweredLaunch:
|
||
case RideMode::LimPoweredLaunch:
|
||
if (numStations <= 1)
|
||
return { true };
|
||
return { false, STR_UNABLE_TO_OPERATE_WITH_MORE_THAN_ONE_STATION_IN_THIS_MODE };
|
||
case RideMode::Shuttle:
|
||
if (numStations >= 2)
|
||
return { true };
|
||
return { false, STR_UNABLE_TO_OPERATE_WITH_LESS_THAN_TWO_STATIONS_IN_THIS_MODE };
|
||
default:
|
||
{
|
||
// This is workaround for multiple compilation errors of type "enumeration value ‘RIDE_MODE_*' not handled
|
||
// in switch [-Werror=switch]"
|
||
}
|
||
}
|
||
|
||
const auto& rtd = ride.GetRideTypeDescriptor();
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_HAS_ONE_STATION) && numStations > 1)
|
||
return { false, STR_UNABLE_TO_OPERATE_WITH_MORE_THAN_ONE_STATION_IN_THIS_MODE };
|
||
|
||
return { true };
|
||
}
|
||
|
||
/**
|
||
* returns stationIndex of first station on success
|
||
* STATION_INDEX_NULL on failure.
|
||
*/
|
||
static StationIndexWithMessage RideModeCheckStationPresent(const Ride& ride)
|
||
{
|
||
auto stationIndex = RideGetFirstValidStationStart(ride);
|
||
|
||
if (stationIndex.IsNull())
|
||
{
|
||
const auto& rtd = ride.GetRideTypeDescriptor();
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_HAS_TRACK))
|
||
return { StationIndex::GetNull(), STR_NOT_YET_CONSTRUCTED };
|
||
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
return { StationIndex::GetNull(), STR_NOT_YET_CONSTRUCTED };
|
||
|
||
return { StationIndex::GetNull(), STR_REQUIRES_A_STATION_PLATFORM };
|
||
}
|
||
|
||
return { stationIndex };
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B5872
|
||
*/
|
||
static ResultWithMessage RideCheckForEntranceExit(RideId rideIndex)
|
||
{
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return { false };
|
||
|
||
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP_OR_FACILITY))
|
||
return { true };
|
||
|
||
uint8_t entrance = 0;
|
||
uint8_t exit = 0;
|
||
for (const auto& station : ride->GetStations())
|
||
{
|
||
if (station.Start.IsNull())
|
||
continue;
|
||
|
||
if (!station.Entrance.IsNull())
|
||
{
|
||
entrance = 1;
|
||
}
|
||
|
||
if (!station.Exit.IsNull())
|
||
{
|
||
exit = 1;
|
||
}
|
||
|
||
// If station start and no entrance/exit
|
||
// Sets same error message as no entrance
|
||
if (station.Exit.IsNull() && station.Entrance.IsNull())
|
||
{
|
||
entrance = 0;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (entrance == 0)
|
||
{
|
||
return { false, STR_ENTRANCE_NOT_YET_BUILT };
|
||
}
|
||
|
||
if (exit == 0)
|
||
{
|
||
return { false, STR_EXIT_NOT_YET_BUILT };
|
||
}
|
||
|
||
return { true };
|
||
}
|
||
|
||
/**
|
||
* Calls FootpathChainRideQueue for all entrances of the ride
|
||
* rct2: 0x006B5952
|
||
*/
|
||
void Ride::ChainQueues() const
|
||
{
|
||
for (const auto& station : stations)
|
||
{
|
||
if (station.Entrance.IsNull())
|
||
continue;
|
||
|
||
auto mapLocation = station.Entrance.ToCoordsXYZ();
|
||
|
||
// This will fire for every entrance on this x, y and z, regardless whether that actually belongs to
|
||
// the ride or not.
|
||
TileElement* tileElement = MapGetFirstElementAt(station.Entrance);
|
||
if (tileElement != nullptr)
|
||
{
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TileElementType::Entrance)
|
||
continue;
|
||
if (tileElement->GetBaseZ() != mapLocation.z)
|
||
continue;
|
||
|
||
int32_t direction = tileElement->GetDirection();
|
||
FootpathChainRideQueue(id, GetStationIndex(&station), mapLocation, tileElement, DirectionReverse(direction));
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006D3319
|
||
*/
|
||
static ResultWithMessage RideCheckBlockBrakes(const CoordsXYE& input, CoordsXYE* output)
|
||
{
|
||
if (input.element == nullptr || input.element->GetType() != TileElementType::Track)
|
||
return { false };
|
||
|
||
RideId rideIndex = input.element->AsTrack()->GetRideIndex();
|
||
WindowBase* w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && _currentRideIndex == rideIndex)
|
||
RideConstructionInvalidateCurrentTrack();
|
||
|
||
TrackCircuitIterator it;
|
||
TrackCircuitIteratorBegin(&it, input);
|
||
while (TrackCircuitIteratorNext(&it))
|
||
{
|
||
if (TrackTypeIsBlockBrakes(it.current.element->AsTrack()->GetTrackType()))
|
||
{
|
||
auto type = it.last.element->AsTrack()->GetTrackType();
|
||
if (type == TrackElemType::EndStation)
|
||
{
|
||
*output = it.current;
|
||
return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_STATION };
|
||
}
|
||
if (TrackTypeIsBlockBrakes(type))
|
||
{
|
||
*output = it.current;
|
||
return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_EACH_OTHER };
|
||
}
|
||
if (it.last.element->AsTrack()->HasChain() && type != TrackElemType::LeftCurvedLiftHill
|
||
&& type != TrackElemType::RightCurvedLiftHill)
|
||
{
|
||
*output = it.current;
|
||
return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_THE_TOP_OF_THIS_LIFT_HILL };
|
||
}
|
||
}
|
||
}
|
||
if (!it.looped)
|
||
{
|
||
// Not sure why this is the case...
|
||
*output = it.last;
|
||
return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_STATION };
|
||
}
|
||
|
||
return { true };
|
||
}
|
||
|
||
/**
|
||
* Iterates along the track until an inversion (loop, corkscrew, barrel roll etc.) track piece is reached.
|
||
* @param input The start track element and position.
|
||
* @param output The first track element and position which is classified as an inversion.
|
||
* @returns true if an inversion track piece is found, otherwise false.
|
||
* rct2: 0x006CB149
|
||
*/
|
||
static bool RideCheckTrackContainsInversions(const CoordsXYE& input, CoordsXYE* output)
|
||
{
|
||
if (input.element == nullptr)
|
||
return false;
|
||
|
||
const auto* trackElement = input.element->AsTrack();
|
||
if (trackElement == nullptr)
|
||
return false;
|
||
|
||
RideId rideIndex = trackElement->GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride != nullptr)
|
||
{
|
||
const auto& rtd = ride->GetRideTypeDescriptor();
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
return true;
|
||
}
|
||
|
||
WindowBase* w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && rideIndex == _currentRideIndex)
|
||
{
|
||
RideConstructionInvalidateCurrentTrack();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
TrackCircuitIterator it, slowIt;
|
||
TrackCircuitIteratorBegin(&it, input);
|
||
slowIt = it;
|
||
|
||
while (TrackCircuitIteratorNext(&it))
|
||
{
|
||
auto trackType = it.current.element->AsTrack()->GetTrackType();
|
||
const auto& ted = GetTrackElementDescriptor(trackType);
|
||
if (ted.Flags & TRACK_ELEM_FLAG_INVERSION_TO_NORMAL)
|
||
{
|
||
*output = it.current;
|
||
return true;
|
||
}
|
||
|
||
// Prevents infinite loops
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
TrackCircuitIteratorNext(&slowIt);
|
||
if (TrackCircuitIteratorsMatch(&it, &slowIt))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Iterates along the track until a banked track piece is reached.
|
||
* @param input The start track element and position.
|
||
* @param output The first track element and position which is banked.
|
||
* @returns true if a banked track piece is found, otherwise false.
|
||
* rct2: 0x006CB1D3
|
||
*/
|
||
static bool RideCheckTrackContainsBanked(const CoordsXYE& input, CoordsXYE* output)
|
||
{
|
||
if (input.element == nullptr)
|
||
return false;
|
||
|
||
const auto* trackElement = input.element->AsTrack();
|
||
if (trackElement == nullptr)
|
||
return false;
|
||
|
||
auto rideIndex = trackElement->GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
const auto& rtd = ride->GetRideTypeDescriptor();
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
return true;
|
||
|
||
WindowBase* w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && rideIndex == _currentRideIndex)
|
||
{
|
||
RideConstructionInvalidateCurrentTrack();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
TrackCircuitIterator it, slowIt;
|
||
TrackCircuitIteratorBegin(&it, input);
|
||
slowIt = it;
|
||
|
||
while (TrackCircuitIteratorNext(&it))
|
||
{
|
||
auto trackType = it.current.element->AsTrack()->GetTrackType();
|
||
const auto& ted = GetTrackElementDescriptor(trackType);
|
||
if (ted.Flags & TRACK_ELEM_FLAG_BANKED)
|
||
{
|
||
*output = it.current;
|
||
return true;
|
||
}
|
||
|
||
// Prevents infinite loops
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
TrackCircuitIteratorNext(&slowIt);
|
||
if (TrackCircuitIteratorsMatch(&it, &slowIt))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006CB25D
|
||
*/
|
||
static int32_t RideCheckStationLength(const CoordsXYE& input, CoordsXYE* output)
|
||
{
|
||
WindowBase* w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0
|
||
&& _currentRideIndex == input.element->AsTrack()->GetRideIndex())
|
||
{
|
||
RideConstructionInvalidateCurrentTrack();
|
||
}
|
||
|
||
output->x = input.x;
|
||
output->y = input.y;
|
||
output->element = input.element;
|
||
TrackBeginEnd trackBeginEnd;
|
||
while (TrackBlockGetPrevious(*output, &trackBeginEnd))
|
||
{
|
||
output->x = trackBeginEnd.begin_x;
|
||
output->y = trackBeginEnd.begin_y;
|
||
output->element = trackBeginEnd.begin_element;
|
||
}
|
||
|
||
int32_t num_station_elements = 0;
|
||
CoordsXYE last_good_station = *output;
|
||
|
||
do
|
||
{
|
||
const auto& ted = GetTrackElementDescriptor(output->element->AsTrack()->GetTrackType());
|
||
if (std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN)
|
||
{
|
||
num_station_elements++;
|
||
last_good_station = *output;
|
||
}
|
||
else
|
||
{
|
||
if (num_station_elements == 0)
|
||
continue;
|
||
if (num_station_elements == 1)
|
||
{
|
||
return 0;
|
||
}
|
||
num_station_elements = 0;
|
||
}
|
||
} while (TrackBlockGetNext(output, output, nullptr, nullptr));
|
||
|
||
// Prevent returning a pointer to a map element with no track.
|
||
*output = last_good_station;
|
||
if (num_station_elements == 1)
|
||
return 0;
|
||
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006CB2DA
|
||
*/
|
||
static bool RideCheckStartAndEndIsStation(const CoordsXYE& input)
|
||
{
|
||
CoordsXYE trackBack, trackFront;
|
||
|
||
RideId rideIndex = input.element->AsTrack()->GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
auto w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && rideIndex == _currentRideIndex)
|
||
{
|
||
RideConstructionInvalidateCurrentTrack();
|
||
}
|
||
|
||
// Check back of the track
|
||
TrackGetBack(input, &trackBack);
|
||
auto trackType = trackBack.element->AsTrack()->GetTrackType();
|
||
const auto* ted = &GetTrackElementDescriptor(trackType);
|
||
if (!(std::get<0>(ted->SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
{
|
||
return false;
|
||
}
|
||
ride->ChairliftBullwheelLocation[0] = TileCoordsXYZ{ CoordsXYZ{ trackBack.x, trackBack.y, trackBack.element->GetBaseZ() } };
|
||
|
||
// Check front of the track
|
||
TrackGetFront(input, &trackFront);
|
||
trackType = trackFront.element->AsTrack()->GetTrackType();
|
||
ted = &GetTrackElementDescriptor(trackType);
|
||
if (!(std::get<0>(ted->SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
{
|
||
return false;
|
||
}
|
||
ride->ChairliftBullwheelLocation[1] = TileCoordsXYZ{ CoordsXYZ{ trackFront.x, trackFront.y,
|
||
trackFront.element->GetBaseZ() } };
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Sets the position and direction of the returning point on the track of a boat hire ride. This will either be the end of the
|
||
* station or the last track piece from the end of the direction.
|
||
* rct2: 0x006B4D39
|
||
*/
|
||
static void RideSetBoatHireReturnPoint(Ride& ride, const CoordsXYE& startElement)
|
||
{
|
||
int32_t trackType = -1;
|
||
auto returnPos = startElement;
|
||
int32_t startX = returnPos.x;
|
||
int32_t startY = returnPos.y;
|
||
TrackBeginEnd trackBeginEnd;
|
||
while (TrackBlockGetPrevious(returnPos, &trackBeginEnd))
|
||
{
|
||
// If previous track is back to the starting x, y, then break loop (otherwise possible infinite loop)
|
||
if (trackType != -1 && startX == trackBeginEnd.begin_x && startY == trackBeginEnd.begin_y)
|
||
break;
|
||
|
||
auto trackCoords = CoordsXYZ{ trackBeginEnd.begin_x, trackBeginEnd.begin_y, trackBeginEnd.begin_z };
|
||
int32_t direction = trackBeginEnd.begin_direction;
|
||
trackType = trackBeginEnd.begin_element->AsTrack()->GetTrackType();
|
||
auto newCoords = GetTrackElementOriginAndApplyChanges(
|
||
{ trackCoords, static_cast<Direction>(direction) }, trackType, 0, &returnPos.element, 0);
|
||
returnPos = newCoords.has_value() ? CoordsXYE{ newCoords.value(), returnPos.element }
|
||
: CoordsXYE{ trackCoords, returnPos.element };
|
||
};
|
||
|
||
trackType = returnPos.element->AsTrack()->GetTrackType();
|
||
const auto& ted = GetTrackElementDescriptor(trackType);
|
||
int32_t elementReturnDirection = ted.Coordinates.rotation_begin;
|
||
ride.boat_hire_return_direction = returnPos.element->GetDirectionWithOffset(elementReturnDirection);
|
||
ride.boat_hire_return_position = TileCoordsXY{ returnPos };
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4D39
|
||
*/
|
||
static void RideSetMazeEntranceExitPoints(Ride& ride)
|
||
{
|
||
// Needs room for an entrance and an exit per station, plus one position for the list terminator.
|
||
TileCoordsXYZD positions[(OpenRCT2::Limits::MaxStationsPerRide * 2) + 1];
|
||
|
||
// Create a list of all the entrance and exit positions
|
||
TileCoordsXYZD* position = positions;
|
||
for (const auto& station : ride.GetStations())
|
||
{
|
||
if (!station.Entrance.IsNull())
|
||
{
|
||
*position++ = station.Entrance;
|
||
}
|
||
if (!station.Exit.IsNull())
|
||
{
|
||
*position++ = station.Exit;
|
||
}
|
||
}
|
||
(*position++).SetNull();
|
||
|
||
// Enumerate entrance and exit positions
|
||
for (position = positions; !(*position).IsNull(); position++)
|
||
{
|
||
auto entranceExitMapPos = position->ToCoordsXYZ();
|
||
|
||
TileElement* tileElement = MapGetFirstElementAt(*position);
|
||
do
|
||
{
|
||
if (tileElement == nullptr)
|
||
break;
|
||
if (tileElement->GetType() != TileElementType::Entrance)
|
||
continue;
|
||
if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_ENTRANCE
|
||
&& tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_EXIT)
|
||
{
|
||
continue;
|
||
}
|
||
if (tileElement->GetBaseZ() != entranceExitMapPos.z)
|
||
continue;
|
||
|
||
MazeEntranceHedgeRemoval({ entranceExitMapPos, tileElement });
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
|
||
void SetBrakeClosedMultiTile(TrackElement& trackElement, const CoordsXY& trackLocation, bool isClosed)
|
||
{
|
||
switch (trackElement.GetTrackType())
|
||
{
|
||
case TrackElemType::DiagUp25ToFlat:
|
||
case TrackElemType::DiagUp60ToFlat:
|
||
case TrackElemType::CableLiftHill:
|
||
case TrackElemType::DiagBrakes:
|
||
case TrackElemType::DiagBlockBrakes:
|
||
GetTrackElementOriginAndApplyChanges(
|
||
{ trackLocation, trackElement.GetBaseZ(), trackElement.GetDirection() }, trackElement.GetTrackType(), isClosed,
|
||
nullptr, TRACK_ELEMENT_SET_BRAKE_CLOSED_STATE);
|
||
break;
|
||
default:
|
||
trackElement.SetBrakeClosed(isClosed);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Opens all block brakes of a ride.
|
||
* rct2: 0x006B4E6B
|
||
*/
|
||
static void RideOpenBlockBrakes(const CoordsXYE& startElement)
|
||
{
|
||
CoordsXYE currentElement = startElement;
|
||
do
|
||
{
|
||
auto trackType = currentElement.element->AsTrack()->GetTrackType();
|
||
switch (trackType)
|
||
{
|
||
case TrackElemType::BlockBrakes:
|
||
case TrackElemType::DiagBlockBrakes:
|
||
BlockBrakeSetLinkedBrakesClosed(
|
||
CoordsXYZ(currentElement.x, currentElement.y, currentElement.element->GetBaseZ()),
|
||
*currentElement.element->AsTrack(), false);
|
||
[[fallthrough]];
|
||
case TrackElemType::DiagUp25ToFlat:
|
||
case TrackElemType::DiagUp60ToFlat:
|
||
case TrackElemType::CableLiftHill:
|
||
case TrackElemType::EndStation:
|
||
case TrackElemType::Up25ToFlat:
|
||
case TrackElemType::Up60ToFlat:
|
||
SetBrakeClosedMultiTile(*currentElement.element->AsTrack(), { currentElement.x, currentElement.y }, false);
|
||
break;
|
||
}
|
||
} while (TrackBlockGetNext(¤tElement, ¤tElement, nullptr, nullptr)
|
||
&& currentElement.element != startElement.element);
|
||
}
|
||
|
||
/**
|
||
* Set the open status of brakes adjacent to the block brake
|
||
*/
|
||
void BlockBrakeSetLinkedBrakesClosed(const CoordsXYZ& vehicleTrackLocation, TrackElement& trackElement, bool isClosed)
|
||
{
|
||
uint8_t brakeSpeed = trackElement.GetBrakeBoosterSpeed();
|
||
|
||
auto tileElement = reinterpret_cast<TileElement*>(&trackElement);
|
||
auto location = vehicleTrackLocation;
|
||
TrackBeginEnd trackBeginEnd, slowTrackBeginEnd;
|
||
TileElement slowTileElement = *tileElement;
|
||
bool counter = true;
|
||
CoordsXY slowLocation = location;
|
||
do
|
||
{
|
||
if (!TrackBlockGetPrevious({ location, tileElement }, &trackBeginEnd))
|
||
{
|
||
return;
|
||
}
|
||
if (trackBeginEnd.begin_x == vehicleTrackLocation.x && trackBeginEnd.begin_y == vehicleTrackLocation.y
|
||
&& tileElement == trackBeginEnd.begin_element)
|
||
{
|
||
return;
|
||
}
|
||
|
||
location.x = trackBeginEnd.end_x;
|
||
location.y = trackBeginEnd.end_y;
|
||
location.z = trackBeginEnd.begin_z;
|
||
tileElement = trackBeginEnd.begin_element;
|
||
|
||
if (TrackTypeIsBrakes(tileElement->AsTrack()->GetTrackType()))
|
||
{
|
||
SetBrakeClosedMultiTile(
|
||
*tileElement->AsTrack(), { trackBeginEnd.begin_x, trackBeginEnd.begin_y },
|
||
(tileElement->AsTrack()->GetBrakeBoosterSpeed() >= brakeSpeed) || isClosed);
|
||
}
|
||
|
||
// prevent infinite loop
|
||
counter = !counter;
|
||
if (counter)
|
||
{
|
||
TrackBlockGetPrevious({ slowLocation, &slowTileElement }, &slowTrackBeginEnd);
|
||
slowLocation.x = slowTrackBeginEnd.end_x;
|
||
slowLocation.y = slowTrackBeginEnd.end_y;
|
||
slowTileElement = *(slowTrackBeginEnd.begin_element);
|
||
if (slowLocation == location && slowTileElement.GetBaseZ() == tileElement->GetBaseZ()
|
||
&& slowTileElement.GetType() == tileElement->GetType()
|
||
&& slowTileElement.GetDirection() == tileElement->GetDirection())
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
} while (TrackTypeIsBrakes(trackBeginEnd.begin_element->AsTrack()->GetTrackType()));
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4D26
|
||
*/
|
||
static void RideSetStartFinishPoints(RideId rideIndex, const CoordsXYE& startElement)
|
||
{
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return;
|
||
|
||
const auto& rtd = ride->GetRideTypeDescriptor();
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
RideSetMazeEntranceExitPoints(*ride);
|
||
else if (ride->type == RIDE_TYPE_BOAT_HIRE)
|
||
RideSetBoatHireReturnPoint(*ride, startElement);
|
||
|
||
if (ride->IsBlockSectioned() && !(ride->lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK))
|
||
{
|
||
RideOpenBlockBrakes(startElement);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x0069ED9E
|
||
*/
|
||
static int32_t count_free_misc_sprite_slots()
|
||
{
|
||
int32_t miscSpriteCount = GetMiscEntityCount();
|
||
int32_t remainingSpriteCount = GetNumFreeEntities();
|
||
return std::max(0, miscSpriteCount + remainingSpriteCount - 300);
|
||
}
|
||
|
||
static constexpr CoordsXY word_9A3AB4[4] = {
|
||
{ 0, 0 },
|
||
{ 0, -96 },
|
||
{ -96, -96 },
|
||
{ -96, 0 },
|
||
};
|
||
|
||
// clang-format off
|
||
static constexpr CoordsXY word_9A2A60[] = {
|
||
{ 0, 16 },
|
||
{ 16, 31 },
|
||
{ 31, 16 },
|
||
{ 16, 0 },
|
||
{ 16, 16 },
|
||
{ 64, 64 },
|
||
{ 64, -32 },
|
||
{ -32, -32 },
|
||
{ -32, 64 },
|
||
};
|
||
// clang-format on
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DD90D
|
||
*/
|
||
static Vehicle* VehicleCreateCar(
|
||
Ride& ride, int32_t carEntryIndex, int32_t carIndex, int32_t vehicleIndex, const CoordsXYZ& carPosition,
|
||
int32_t* remainingDistance, TrackElement* trackElement)
|
||
{
|
||
if (trackElement == nullptr)
|
||
return nullptr;
|
||
|
||
auto rideEntry = ride.GetRideEntry();
|
||
if (rideEntry == nullptr)
|
||
return nullptr;
|
||
|
||
auto& carEntry = rideEntry->Cars[carEntryIndex];
|
||
|
||
auto* vehicle = CreateEntity<Vehicle>();
|
||
if (vehicle == nullptr)
|
||
return nullptr;
|
||
|
||
vehicle->ride = ride.id;
|
||
vehicle->ride_subtype = ride.subtype;
|
||
|
||
vehicle->vehicle_type = carEntryIndex;
|
||
vehicle->SubType = carIndex == 0 ? Vehicle::Type::Head : Vehicle::Type::Tail;
|
||
vehicle->var_44 = Numerics::ror32(carEntry.spacing, 10) & 0xFFFF;
|
||
|
||
const auto halfSpacing = carEntry.spacing >> 1;
|
||
*remainingDistance -= halfSpacing;
|
||
vehicle->remaining_distance = *remainingDistance;
|
||
|
||
if (!(carEntry.flags & CAR_ENTRY_FLAG_GO_KART))
|
||
{
|
||
*remainingDistance -= halfSpacing;
|
||
}
|
||
|
||
// Loc6DD9A5:
|
||
vehicle->SpriteData.Width = carEntry.sprite_width;
|
||
vehicle->SpriteData.HeightMin = carEntry.sprite_height_negative;
|
||
vehicle->SpriteData.HeightMax = carEntry.sprite_height_positive;
|
||
vehicle->mass = carEntry.car_mass;
|
||
vehicle->num_seats = carEntry.num_seats;
|
||
vehicle->speed = carEntry.powered_max_speed;
|
||
vehicle->powered_acceleration = carEntry.powered_acceleration;
|
||
vehicle->velocity = 0;
|
||
vehicle->acceleration = 0;
|
||
vehicle->SwingSprite = 0;
|
||
vehicle->SwingPosition = 0;
|
||
vehicle->SwingSpeed = 0;
|
||
vehicle->restraints_position = 0;
|
||
vehicle->spin_sprite = 0;
|
||
vehicle->spin_speed = 0;
|
||
vehicle->sound2_flags = 0;
|
||
vehicle->sound1_id = OpenRCT2::Audio::SoundId::Null;
|
||
vehicle->sound2_id = OpenRCT2::Audio::SoundId::Null;
|
||
vehicle->next_vehicle_on_train = EntityId::GetNull();
|
||
vehicle->CollisionDetectionTimer = 0;
|
||
vehicle->animation_frame = 0;
|
||
vehicle->animationState = 0;
|
||
vehicle->scream_sound_id = OpenRCT2::Audio::SoundId::Null;
|
||
vehicle->Pitch = 0;
|
||
vehicle->bank_rotation = 0;
|
||
vehicle->target_seat_rotation = 4;
|
||
vehicle->seat_rotation = 4;
|
||
for (size_t i = 0; i < std::size(vehicle->peep); i++)
|
||
{
|
||
vehicle->peep[i] = EntityId::GetNull();
|
||
}
|
||
|
||
const auto& rtd = ride.GetRideTypeDescriptor();
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_DODGEM_CAR_PLACEMENT)
|
||
{
|
||
// Loc6DDCA4:
|
||
vehicle->TrackSubposition = VehicleTrackSubposition::Default;
|
||
int32_t direction = trackElement->GetDirection();
|
||
auto dodgemPos = carPosition + CoordsXYZ{ word_9A3AB4[direction], 0 };
|
||
vehicle->TrackLocation = dodgemPos;
|
||
vehicle->current_station = trackElement->GetStationIndex();
|
||
|
||
dodgemPos.z += rtd.Heights.VehicleZOffset;
|
||
|
||
vehicle->SetTrackDirection(0);
|
||
vehicle->SetTrackType(trackElement->GetTrackType());
|
||
vehicle->track_progress = 0;
|
||
vehicle->SetState(Vehicle::Status::MovingToEndOfStation);
|
||
vehicle->Flags = 0;
|
||
|
||
CoordsXY chosenLoc;
|
||
auto numAttempts = 0;
|
||
// Loc6DDD26:
|
||
do
|
||
{
|
||
numAttempts++;
|
||
// This can happen when trying to spawn dozens of cars in a tiny area.
|
||
if (numAttempts > 10000)
|
||
return nullptr;
|
||
|
||
vehicle->Orientation = ScenarioRand() & 0x1E;
|
||
chosenLoc.y = dodgemPos.y + (ScenarioRand() & 0xFF);
|
||
chosenLoc.x = dodgemPos.x + (ScenarioRand() & 0xFF);
|
||
} while (vehicle->DodgemsCarWouldCollideAt(chosenLoc).has_value());
|
||
|
||
vehicle->MoveTo({ chosenLoc, dodgemPos.z });
|
||
}
|
||
else
|
||
{
|
||
VehicleTrackSubposition subposition = VehicleTrackSubposition::Default;
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_CHAIRLIFT)
|
||
{
|
||
subposition = VehicleTrackSubposition::ChairliftGoingOut;
|
||
}
|
||
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_GO_KART)
|
||
{
|
||
// Choose which lane Go Kart should start in
|
||
subposition = VehicleTrackSubposition::GoKartsLeftLane;
|
||
if (vehicleIndex & 1)
|
||
{
|
||
subposition = VehicleTrackSubposition::GoKartsRightLane;
|
||
}
|
||
}
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_MINI_GOLF)
|
||
{
|
||
subposition = VehicleTrackSubposition::MiniGolfStart9;
|
||
vehicle->var_D3 = 0;
|
||
vehicle->mini_golf_current_animation = MiniGolfAnimation::Walk;
|
||
vehicle->mini_golf_flags = 0;
|
||
}
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_REVERSER_BOGIE)
|
||
{
|
||
if (vehicle->IsHead())
|
||
{
|
||
subposition = VehicleTrackSubposition::ReverserRCFrontBogie;
|
||
}
|
||
}
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_REVERSER_PASSENGER_CAR)
|
||
{
|
||
subposition = VehicleTrackSubposition::ReverserRCRearBogie;
|
||
}
|
||
vehicle->TrackSubposition = subposition;
|
||
|
||
auto chosenLoc = carPosition;
|
||
vehicle->TrackLocation = chosenLoc;
|
||
|
||
int32_t direction = trackElement->GetDirection();
|
||
vehicle->Orientation = direction << 3;
|
||
|
||
if (ride.type == RIDE_TYPE_SPACE_RINGS)
|
||
{
|
||
direction = 4;
|
||
}
|
||
else
|
||
{
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_VEHICLE_IS_INTEGRAL))
|
||
{
|
||
if (rtd.StartTrackPiece != TrackElemType::FlatTrack1x4B)
|
||
{
|
||
if (rtd.StartTrackPiece != TrackElemType::FlatTrack1x4A)
|
||
{
|
||
if (ride.type == RIDE_TYPE_ENTERPRISE)
|
||
{
|
||
direction += 5;
|
||
}
|
||
else
|
||
{
|
||
direction = 4;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
chosenLoc += CoordsXYZ{ word_9A2A60[direction], rtd.Heights.VehicleZOffset };
|
||
|
||
vehicle->current_station = trackElement->GetStationIndex();
|
||
|
||
vehicle->MoveTo(chosenLoc);
|
||
vehicle->SetTrackType(trackElement->GetTrackType());
|
||
vehicle->SetTrackDirection(vehicle->Orientation >> 3);
|
||
vehicle->track_progress = 31;
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_MINI_GOLF)
|
||
{
|
||
vehicle->track_progress = 15;
|
||
}
|
||
vehicle->Flags = VehicleFlags::CollisionDisabled;
|
||
if (carEntry.flags & CAR_ENTRY_FLAG_HAS_INVERTED_SPRITE_SET)
|
||
{
|
||
if (trackElement->IsInverted())
|
||
{
|
||
vehicle->SetFlag(VehicleFlags::CarIsInverted);
|
||
}
|
||
}
|
||
vehicle->SetState(Vehicle::Status::MovingToEndOfStation);
|
||
|
||
if (ride.HasLifecycleFlag(RIDE_LIFECYCLE_REVERSED_TRAINS))
|
||
{
|
||
vehicle->SubType = carIndex == (ride.num_cars_per_train - 1) ? Vehicle::Type::Head : Vehicle::Type::Tail;
|
||
vehicle->SetFlag(VehicleFlags::CarIsReversed);
|
||
}
|
||
}
|
||
|
||
// Loc6DDD5E:
|
||
vehicle->num_peeps = 0;
|
||
vehicle->next_free_seat = 0;
|
||
vehicle->BoatLocation.SetNull();
|
||
return vehicle;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DD84C
|
||
*/
|
||
static TrainReference VehicleCreateTrain(
|
||
Ride& ride, const CoordsXYZ& trainPos, int32_t vehicleIndex, int32_t* remainingDistance, TrackElement* trackElement)
|
||
{
|
||
TrainReference train = { nullptr, nullptr };
|
||
bool isReversed = ride.HasLifecycleFlag(RIDE_LIFECYCLE_REVERSED_TRAINS);
|
||
|
||
for (int32_t carIndex = 0; carIndex < ride.num_cars_per_train; carIndex++)
|
||
{
|
||
auto carSpawnIndex = (isReversed) ? (ride.num_cars_per_train - 1) - carIndex : carIndex;
|
||
|
||
auto vehicle = RideEntryGetVehicleAtPosition(ride.subtype, ride.num_cars_per_train, carSpawnIndex);
|
||
auto car = VehicleCreateCar(ride, vehicle, carSpawnIndex, vehicleIndex, trainPos, remainingDistance, trackElement);
|
||
if (car == nullptr)
|
||
break;
|
||
|
||
if (carIndex == 0)
|
||
{
|
||
train.head = car;
|
||
}
|
||
else
|
||
{
|
||
// Link the previous car with this car
|
||
train.tail->next_vehicle_on_train = car->Id;
|
||
train.tail->next_vehicle_on_ride = car->Id;
|
||
car->prev_vehicle_on_ride = train.tail->Id;
|
||
}
|
||
train.tail = car;
|
||
}
|
||
|
||
return train;
|
||
}
|
||
|
||
static bool VehicleCreateTrains(Ride& ride, const CoordsXYZ& trainsPos, TrackElement* trackElement)
|
||
{
|
||
TrainReference firstTrain = {};
|
||
TrainReference lastTrain = {};
|
||
int32_t remainingDistance = 0;
|
||
bool allTrainsCreated = true;
|
||
|
||
for (int32_t vehicleIndex = 0; vehicleIndex < ride.NumTrains; vehicleIndex++)
|
||
{
|
||
if (ride.IsBlockSectioned())
|
||
{
|
||
remainingDistance = 0;
|
||
}
|
||
TrainReference train = VehicleCreateTrain(ride, trainsPos, vehicleIndex, &remainingDistance, trackElement);
|
||
if (train.head == nullptr || train.tail == nullptr)
|
||
{
|
||
allTrainsCreated = false;
|
||
continue;
|
||
}
|
||
|
||
if (vehicleIndex == 0)
|
||
{
|
||
firstTrain = train;
|
||
}
|
||
else
|
||
{
|
||
// Link the end of the previous train with the front of this train
|
||
lastTrain.tail->next_vehicle_on_ride = train.head->Id;
|
||
train.head->prev_vehicle_on_ride = lastTrain.tail->Id;
|
||
}
|
||
lastTrain = train;
|
||
|
||
for (int32_t i = 0; i <= OpenRCT2::Limits::MaxTrainsPerRide; i++)
|
||
{
|
||
if (ride.vehicles[i].IsNull())
|
||
{
|
||
ride.vehicles[i] = train.head->Id;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Link the first train and last train together. Nullptr checks are there to keep Clang happy.
|
||
if (lastTrain.tail != nullptr)
|
||
firstTrain.head->prev_vehicle_on_ride = lastTrain.tail->Id;
|
||
if (firstTrain.head != nullptr)
|
||
lastTrain.tail->next_vehicle_on_ride = firstTrain.head->Id;
|
||
|
||
return allTrainsCreated;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DDE9E
|
||
*/
|
||
static void RideCreateVehiclesFindFirstBlock(const Ride& ride, CoordsXYE* outXYElement)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(ride.vehicles[0]);
|
||
if (vehicle == nullptr)
|
||
return;
|
||
|
||
auto curTrackPos = vehicle->TrackLocation;
|
||
auto curTrackElement = MapGetTrackElementAt(curTrackPos);
|
||
|
||
assert(curTrackElement != nullptr);
|
||
|
||
CoordsXY trackPos = curTrackPos;
|
||
auto trackElement = curTrackElement;
|
||
TrackBeginEnd trackBeginEnd;
|
||
while (TrackBlockGetPrevious({ trackPos, reinterpret_cast<TileElement*>(trackElement) }, &trackBeginEnd))
|
||
{
|
||
trackPos = { trackBeginEnd.end_x, trackBeginEnd.end_y };
|
||
trackElement = trackBeginEnd.begin_element->AsTrack();
|
||
if (trackPos == curTrackPos && trackElement == curTrackElement)
|
||
{
|
||
break;
|
||
}
|
||
|
||
auto trackType = trackElement->GetTrackType();
|
||
switch (trackType)
|
||
{
|
||
case TrackElemType::DiagUp25ToFlat:
|
||
case TrackElemType::DiagUp60ToFlat:
|
||
if (!trackElement->HasChain())
|
||
{
|
||
break;
|
||
}
|
||
[[fallthrough]];
|
||
case TrackElemType::DiagBlockBrakes:
|
||
{
|
||
TileElement* tileElement = MapGetTrackElementAtOfTypeSeq(
|
||
{ trackBeginEnd.begin_x, trackBeginEnd.begin_y, trackBeginEnd.begin_z }, trackType, 0);
|
||
|
||
if (tileElement != nullptr)
|
||
{
|
||
outXYElement->x = trackBeginEnd.begin_x;
|
||
outXYElement->y = trackBeginEnd.begin_y;
|
||
outXYElement->element = tileElement;
|
||
return;
|
||
}
|
||
break;
|
||
}
|
||
case TrackElemType::Up25ToFlat:
|
||
case TrackElemType::Up60ToFlat:
|
||
if (!trackElement->HasChain())
|
||
{
|
||
break;
|
||
}
|
||
[[fallthrough]];
|
||
case TrackElemType::EndStation:
|
||
case TrackElemType::CableLiftHill:
|
||
case TrackElemType::BlockBrakes:
|
||
*outXYElement = { trackPos, reinterpret_cast<TileElement*>(trackElement) };
|
||
return;
|
||
}
|
||
}
|
||
|
||
outXYElement->x = curTrackPos.x;
|
||
outXYElement->y = curTrackPos.y;
|
||
outXYElement->element = reinterpret_cast<TileElement*>(curTrackElement);
|
||
}
|
||
|
||
/**
|
||
* Create and place the rides vehicles
|
||
* rct2: 0x006DD84C
|
||
*/
|
||
ResultWithMessage Ride::CreateVehicles(const CoordsXYE& element, bool isApplying)
|
||
{
|
||
UpdateMaxVehicles();
|
||
if (subtype == OBJECT_ENTRY_INDEX_NULL)
|
||
{
|
||
return { true };
|
||
}
|
||
|
||
// Check if there are enough free sprite slots for all the vehicles
|
||
int32_t totalCars = NumTrains * num_cars_per_train;
|
||
if (totalCars > count_free_misc_sprite_slots())
|
||
{
|
||
return { false, STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES };
|
||
}
|
||
|
||
if (!isApplying)
|
||
{
|
||
return { true };
|
||
}
|
||
|
||
auto* trackElement = element.element->AsTrack();
|
||
auto vehiclePos = CoordsXYZ{ element, element.element->GetBaseZ() };
|
||
int32_t direction = trackElement->GetDirection();
|
||
|
||
//
|
||
if (mode == RideMode::StationToStation)
|
||
{
|
||
vehiclePos -= CoordsXYZ{ CoordsDirectionDelta[direction], 0 };
|
||
|
||
trackElement = MapGetTrackElementAt(vehiclePos);
|
||
|
||
vehiclePos.z = trackElement->GetBaseZ();
|
||
}
|
||
|
||
if (!VehicleCreateTrains(*this, vehiclePos, trackElement))
|
||
{
|
||
// This flag is needed for Ride::RemoveVehicles()
|
||
lifecycle_flags |= RIDE_LIFECYCLE_ON_TRACK;
|
||
RemoveVehicles();
|
||
return { false, STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES };
|
||
}
|
||
// return true;
|
||
|
||
// Initialise station departs
|
||
// 006DDDD0:
|
||
lifecycle_flags |= RIDE_LIFECYCLE_ON_TRACK;
|
||
for (int32_t i = 0; i < OpenRCT2::Limits::MaxStationsPerRide; i++)
|
||
{
|
||
stations[i].Depart = (stations[i].Depart & kStationDepartFlag) | 1;
|
||
}
|
||
|
||
//
|
||
if (type != RIDE_TYPE_SPACE_RINGS && !GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_VEHICLE_IS_INTEGRAL))
|
||
{
|
||
if (IsBlockSectioned())
|
||
{
|
||
CoordsXYE firstBlock{};
|
||
RideCreateVehiclesFindFirstBlock(*this, &firstBlock);
|
||
MoveTrainsToBlockBrakes(
|
||
{ firstBlock.x, firstBlock.y, firstBlock.element->GetBaseZ() }, *firstBlock.element->AsTrack());
|
||
}
|
||
else
|
||
{
|
||
for (int32_t i = 0; i < NumTrains; i++)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(vehicles[i]);
|
||
if (vehicle == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
auto carEntry = vehicle->Entry();
|
||
|
||
if (!(carEntry->flags & CAR_ENTRY_FLAG_DODGEM_CAR_PLACEMENT))
|
||
{
|
||
vehicle->UpdateTrackMotion(nullptr);
|
||
}
|
||
|
||
vehicle->EnableCollisionsForTrain();
|
||
}
|
||
}
|
||
}
|
||
RideUpdateVehicleColours(*this);
|
||
return { true };
|
||
}
|
||
|
||
/**
|
||
* Move all the trains so each one will be placed at the block brake of a different block.
|
||
* The first vehicle will placed into the first block and all other vehicles in the blocks
|
||
* preceding that block.
|
||
* rct2: 0x006DDF9C
|
||
*/
|
||
void Ride::MoveTrainsToBlockBrakes(const CoordsXYZ& firstBlockPosition, TrackElement& firstBlock)
|
||
{
|
||
for (int32_t i = 0; i < NumTrains; i++)
|
||
{
|
||
auto train = GetEntity<Vehicle>(vehicles[i]);
|
||
if (train == nullptr)
|
||
continue;
|
||
|
||
// At this point, all vehicles have state of MovingToEndOfStation, which slowly moves forward at a constant speed
|
||
// regardless of incline. The first vehicle stops at the station immediately, while all other vehicles seek forward
|
||
// until they reach a closed block brake. The block brake directly before the station is set to closed every frame
|
||
// because the trains will open the block brake when the tail leaves the station. Brakes have no effect at this time, so
|
||
// do not set linked brakes when closing the first block.
|
||
train->UpdateTrackMotion(nullptr);
|
||
|
||
if (i == 0)
|
||
{
|
||
train->EnableCollisionsForTrain();
|
||
continue;
|
||
}
|
||
|
||
size_t numIterations = 0;
|
||
do
|
||
{
|
||
// Fixes both freezing issues in #15503.
|
||
// TODO: refactor the code so a tortoise-and-hare algorithm can be used.
|
||
if (numIterations++ > 1000000)
|
||
{
|
||
break;
|
||
}
|
||
|
||
firstBlock.SetBrakeClosed(true);
|
||
for (Vehicle* car = train; car != nullptr; car = GetEntity<Vehicle>(car->next_vehicle_on_train))
|
||
{
|
||
car->velocity = 0;
|
||
car->acceleration = 0;
|
||
car->SwingSprite = 0;
|
||
car->remaining_distance += 13962;
|
||
}
|
||
} while (!(train->UpdateTrackMotion(nullptr) & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_BLOCK_BRAKE));
|
||
|
||
// All vehicles are in position, set the block brake directly before the station one last time and make sure the brakes
|
||
// are set appropriately
|
||
SetBrakeClosedMultiTile(firstBlock, firstBlockPosition, true);
|
||
if (TrackTypeIsBlockBrakes(firstBlock.GetTrackType()))
|
||
{
|
||
BlockBrakeSetLinkedBrakesClosed(firstBlockPosition, firstBlock, true);
|
||
}
|
||
for (Vehicle* car = train; car != nullptr; car = GetEntity<Vehicle>(car->next_vehicle_on_train))
|
||
{
|
||
car->ClearFlag(VehicleFlags::CollisionDisabled);
|
||
car->SetState(Vehicle::Status::Travelling, car->sub_state);
|
||
if ((car->GetTrackType()) == TrackElemType::EndStation)
|
||
{
|
||
car->SetState(Vehicle::Status::MovingToEndOfStation, car->sub_state);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Checks and initialises the cable lift track returns false if unable to find
|
||
* appropriate track.
|
||
* rct2: 0x006D31A6
|
||
*/
|
||
static ResultWithMessage RideInitialiseCableLiftTrack(const Ride& ride, bool isApplying)
|
||
{
|
||
CoordsXYZ location;
|
||
location.SetNull();
|
||
for (const auto& station : ride.GetStations())
|
||
{
|
||
location = station.GetStart();
|
||
if (!location.IsNull())
|
||
break;
|
||
}
|
||
|
||
if (location.IsNull())
|
||
{
|
||
return { false, STR_CABLE_LIFT_HILL_MUST_START_IMMEDIATELY_AFTER_STATION };
|
||
}
|
||
|
||
bool success = false;
|
||
TileElement* tileElement = MapGetFirstElementAt(location);
|
||
if (tileElement == nullptr)
|
||
return { false };
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TileElementType::Track)
|
||
continue;
|
||
if (tileElement->GetBaseZ() != location.z)
|
||
continue;
|
||
|
||
const auto& ted = GetTrackElementDescriptor(tileElement->AsTrack()->GetTrackType());
|
||
if (!(std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
{
|
||
continue;
|
||
}
|
||
success = true;
|
||
break;
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
|
||
if (!success)
|
||
return { false };
|
||
|
||
enum
|
||
{
|
||
STATE_FIND_CABLE_LIFT,
|
||
STATE_FIND_STATION,
|
||
STATE_REST_OF_TRACK
|
||
};
|
||
int32_t state = STATE_FIND_CABLE_LIFT;
|
||
|
||
TrackCircuitIterator it;
|
||
TrackCircuitIteratorBegin(&it, { location, tileElement });
|
||
while (TrackCircuitIteratorPrevious(&it))
|
||
{
|
||
tileElement = it.current.element;
|
||
auto trackType = tileElement->AsTrack()->GetTrackType();
|
||
|
||
uint16_t flags = TRACK_ELEMENT_SET_HAS_CABLE_LIFT_FALSE;
|
||
switch (state)
|
||
{
|
||
case STATE_FIND_CABLE_LIFT:
|
||
// Search for a cable lift hill track element
|
||
if (trackType == TrackElemType::CableLiftHill)
|
||
{
|
||
flags = TRACK_ELEMENT_SET_HAS_CABLE_LIFT_TRUE;
|
||
state = STATE_FIND_STATION;
|
||
}
|
||
break;
|
||
case STATE_FIND_STATION:
|
||
// Search for the start of the hill
|
||
switch (trackType)
|
||
{
|
||
case TrackElemType::Flat:
|
||
case TrackElemType::Up25:
|
||
case TrackElemType::Up60:
|
||
case TrackElemType::FlatToUp25:
|
||
case TrackElemType::Up25ToFlat:
|
||
case TrackElemType::Up25ToUp60:
|
||
case TrackElemType::Up60ToUp25:
|
||
case TrackElemType::FlatToUp60LongBase:
|
||
flags = TRACK_ELEMENT_SET_HAS_CABLE_LIFT_TRUE;
|
||
break;
|
||
case TrackElemType::EndStation:
|
||
state = STATE_REST_OF_TRACK;
|
||
break;
|
||
default:
|
||
return { false, STR_CABLE_LIFT_HILL_MUST_START_IMMEDIATELY_AFTER_STATION };
|
||
}
|
||
break;
|
||
}
|
||
if (isApplying)
|
||
{
|
||
auto tmpLoc = CoordsXYZ{ it.current, tileElement->GetBaseZ() };
|
||
auto direction = tileElement->GetDirection();
|
||
trackType = tileElement->AsTrack()->GetTrackType();
|
||
GetTrackElementOriginAndApplyChanges({ tmpLoc, direction }, trackType, 0, &tileElement, flags);
|
||
}
|
||
}
|
||
return { true };
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DF4D4
|
||
*/
|
||
static ResultWithMessage RideCreateCableLift(RideId rideIndex, bool isApplying)
|
||
{
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride == nullptr)
|
||
return { false };
|
||
|
||
if (ride->mode != RideMode::ContinuousCircuitBlockSectioned && ride->mode != RideMode::ContinuousCircuit)
|
||
{
|
||
return { false, STR_CABLE_LIFT_UNABLE_TO_WORK_IN_THIS_OPERATING_MODE };
|
||
}
|
||
|
||
if (ride->num_circuits > 1)
|
||
{
|
||
return { false, STR_MULTICIRCUIT_NOT_POSSIBLE_WITH_CABLE_LIFT_HILL };
|
||
}
|
||
|
||
if (count_free_misc_sprite_slots() <= 5)
|
||
{
|
||
return { false, STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES };
|
||
}
|
||
|
||
auto cableLiftInitialiseResult = RideInitialiseCableLiftTrack(*ride, isApplying);
|
||
if (!cableLiftInitialiseResult.Successful)
|
||
{
|
||
return { false, cableLiftInitialiseResult.Message };
|
||
}
|
||
|
||
if (!isApplying)
|
||
{
|
||
return { true };
|
||
}
|
||
|
||
auto cableLiftLoc = ride->CableLiftLoc;
|
||
auto tileElement = MapGetTrackElementAt(cableLiftLoc);
|
||
int32_t direction = tileElement->GetDirection();
|
||
|
||
Vehicle* head = nullptr;
|
||
Vehicle* tail = nullptr;
|
||
uint32_t ebx = 0;
|
||
for (int32_t i = 0; i < 5; i++)
|
||
{
|
||
uint32_t edx = Numerics::ror32(0x15478, 10);
|
||
uint16_t var_44 = edx & 0xFFFF;
|
||
edx = Numerics::rol32(edx, 10) >> 1;
|
||
ebx -= edx;
|
||
int32_t remaining_distance = ebx;
|
||
ebx -= edx;
|
||
|
||
Vehicle* current = CableLiftSegmentCreate(
|
||
*ride, cableLiftLoc.x, cableLiftLoc.y, cableLiftLoc.z / 8, direction, var_44, remaining_distance, i == 0);
|
||
current->next_vehicle_on_train = EntityId::GetNull();
|
||
if (i == 0)
|
||
{
|
||
head = current;
|
||
}
|
||
else
|
||
{
|
||
tail->next_vehicle_on_train = current->Id;
|
||
tail->next_vehicle_on_ride = current->Id;
|
||
current->prev_vehicle_on_ride = tail->Id;
|
||
}
|
||
tail = current;
|
||
}
|
||
head->prev_vehicle_on_ride = tail->Id;
|
||
tail->next_vehicle_on_ride = head->Id;
|
||
|
||
ride->lifecycle_flags |= RIDE_LIFECYCLE_CABLE_LIFT;
|
||
head->CableLiftUpdateTrackMotion();
|
||
return { true };
|
||
}
|
||
|
||
/**
|
||
* Opens the construction window prompting to construct a missing entrance or exit.
|
||
* This will also move the screen to the first station missing the entrance or exit.
|
||
* rct2: 0x006B51C0
|
||
*/
|
||
void Ride::ConstructMissingEntranceOrExit() const
|
||
{
|
||
auto* w = WindowGetMain();
|
||
if (w == nullptr)
|
||
return;
|
||
|
||
int8_t entranceOrExit = -1;
|
||
const RideStation* incompleteStation = nullptr;
|
||
for (const auto& station : stations)
|
||
{
|
||
if (station.Start.IsNull())
|
||
continue;
|
||
|
||
if (station.Entrance.IsNull())
|
||
{
|
||
entranceOrExit = WC_RIDE_CONSTRUCTION__WIDX_ENTRANCE;
|
||
incompleteStation = &station;
|
||
break;
|
||
}
|
||
|
||
if (station.Exit.IsNull())
|
||
{
|
||
entranceOrExit = WC_RIDE_CONSTRUCTION__WIDX_EXIT;
|
||
incompleteStation = &station;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (incompleteStation == nullptr)
|
||
{ // No station with a missing entrance or exit was found
|
||
return;
|
||
}
|
||
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
{
|
||
auto location = incompleteStation->GetStart();
|
||
WindowScrollToLocation(*w, location);
|
||
|
||
CoordsXYE trackElement;
|
||
RideTryGetOriginElement(*this, &trackElement);
|
||
FindTrackGap(trackElement, &trackElement);
|
||
int32_t ok = RideModify(trackElement);
|
||
if (ok == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr)
|
||
w->OnMouseUp(entranceOrExit);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B528A
|
||
*/
|
||
static void RideScrollToTrackError(const CoordsXYE& trackElement)
|
||
{
|
||
if (trackElement.element == nullptr)
|
||
return;
|
||
|
||
auto* w = WindowGetMain();
|
||
if (w != nullptr)
|
||
{
|
||
WindowScrollToLocation(*w, { trackElement, trackElement.element->GetBaseZ() });
|
||
RideModify(trackElement);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4F6B
|
||
*/
|
||
TrackElement* Ride::GetOriginElement(StationIndex stationIndex) const
|
||
{
|
||
auto stationLoc = GetStation(stationIndex).Start;
|
||
TileElement* tileElement = MapGetFirstElementAt(stationLoc);
|
||
if (tileElement == nullptr)
|
||
return nullptr;
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TileElementType::Track)
|
||
continue;
|
||
|
||
auto* trackElement = tileElement->AsTrack();
|
||
const auto& ted = GetTrackElementDescriptor(trackElement->GetTrackType());
|
||
if (!(std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
continue;
|
||
|
||
if (trackElement->GetRideIndex() == id)
|
||
return trackElement;
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
ResultWithMessage Ride::Test(bool isApplying)
|
||
{
|
||
if (type == RIDE_TYPE_NULL)
|
||
{
|
||
LOG_WARNING("Invalid ride type for ride %u", id.ToUnderlying());
|
||
return { false };
|
||
}
|
||
|
||
WindowCloseByNumber(WindowClass::RideConstruction, id.ToUnderlying());
|
||
|
||
StationIndex stationIndex = {};
|
||
auto message = ChangeStatusDoStationChecks(stationIndex);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
auto entranceExitCheck = RideCheckForEntranceExit(id);
|
||
if (!entranceExitCheck.Successful)
|
||
{
|
||
ConstructMissingEntranceOrExit();
|
||
return { false, entranceExitCheck.Message };
|
||
}
|
||
|
||
CoordsXYE trackElement = {};
|
||
message = ChangeStatusGetStartElement(stationIndex, trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
message = ChangeStatusCheckCompleteCircuit(trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
message = ChangeStatusCheckTrackValidity(trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
return ChangeStatusCreateVehicles(isApplying, trackElement);
|
||
}
|
||
|
||
ResultWithMessage Ride::Simulate(bool isApplying)
|
||
{
|
||
CoordsXYE trackElement, problematicTrackElement = {};
|
||
if (type == RIDE_TYPE_NULL)
|
||
{
|
||
LOG_WARNING("Invalid ride type for ride %u", id.ToUnderlying());
|
||
return { false };
|
||
}
|
||
|
||
StationIndex stationIndex = {};
|
||
auto message = ChangeStatusDoStationChecks(stationIndex);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
message = ChangeStatusGetStartElement(stationIndex, trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
if (IsBlockSectioned() && FindTrackGap(trackElement, &problematicTrackElement))
|
||
{
|
||
RideScrollToTrackError(problematicTrackElement);
|
||
return { false, STR_TRACK_IS_NOT_A_COMPLETE_CIRCUIT };
|
||
}
|
||
|
||
message = ChangeStatusCheckTrackValidity(trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
return ChangeStatusCreateVehicles(isApplying, trackElement);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4EEA
|
||
*/
|
||
ResultWithMessage Ride::Open(bool isApplying)
|
||
{
|
||
// Check to see if construction tool is in use. If it is close the construction window
|
||
// to set the track to its final state and clean up ghosts.
|
||
// We can't just call close as it would cause a stack overflow during shop creation
|
||
// with auto open on.
|
||
if (WindowClass::RideConstruction == gCurrentToolWidget.window_classification
|
||
&& id.ToUnderlying() == gCurrentToolWidget.window_number && (InputTestFlag(INPUT_FLAG_TOOL_ACTIVE)))
|
||
{
|
||
WindowCloseByNumber(WindowClass::RideConstruction, id.ToUnderlying());
|
||
}
|
||
|
||
StationIndex stationIndex = {};
|
||
auto message = ChangeStatusDoStationChecks(stationIndex);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
auto entranceExitCheck = RideCheckForEntranceExit(id);
|
||
if (!entranceExitCheck.Successful)
|
||
{
|
||
ConstructMissingEntranceOrExit();
|
||
return { false, entranceExitCheck.Message };
|
||
}
|
||
|
||
if (isApplying)
|
||
{
|
||
ChainQueues();
|
||
lifecycle_flags |= RIDE_LIFECYCLE_EVER_BEEN_OPENED;
|
||
}
|
||
|
||
CoordsXYE trackElement = {};
|
||
message = ChangeStatusGetStartElement(stationIndex, trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
message = ChangeStatusCheckCompleteCircuit(trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
message = ChangeStatusCheckTrackValidity(trackElement);
|
||
if (!message.Successful)
|
||
{
|
||
return message;
|
||
}
|
||
|
||
return ChangeStatusCreateVehicles(isApplying, trackElement);
|
||
}
|
||
|
||
/**
|
||
* Given a track element of the ride, find the start of the track.
|
||
* It has to do this as a backwards loop in case this is an incomplete track.
|
||
*/
|
||
void RideGetStartOfTrack(CoordsXYE* output)
|
||
{
|
||
TrackBeginEnd trackBeginEnd;
|
||
CoordsXYE trackElement = *output;
|
||
if (TrackBlockGetPrevious(trackElement, &trackBeginEnd))
|
||
{
|
||
TileElement* initial_map = trackElement.element;
|
||
TrackBeginEnd slowIt = trackBeginEnd;
|
||
bool moveSlowIt = true;
|
||
do
|
||
{
|
||
// Because we are working backwards, begin_element is the section at the end of a piece of track, whereas
|
||
// begin_x and begin_y are the coordinates at the start of a piece of track, so we need to pass end_x and
|
||
// end_y
|
||
CoordsXYE lastGood = {
|
||
/* .x = */ trackBeginEnd.end_x,
|
||
/* .y = */ trackBeginEnd.end_y,
|
||
/* .element = */ trackBeginEnd.begin_element,
|
||
};
|
||
|
||
if (!TrackBlockGetPrevious(
|
||
{ trackBeginEnd.end_x, trackBeginEnd.end_y, trackBeginEnd.begin_element }, &trackBeginEnd))
|
||
{
|
||
trackElement = lastGood;
|
||
break;
|
||
}
|
||
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
if (!TrackBlockGetPrevious({ slowIt.end_x, slowIt.end_y, slowIt.begin_element }, &slowIt)
|
||
|| slowIt.begin_element == trackBeginEnd.begin_element)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
} while (initial_map != trackBeginEnd.begin_element);
|
||
}
|
||
*output = trackElement;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x00696707
|
||
*/
|
||
void Ride::StopGuestsQueuing()
|
||
{
|
||
for (auto peep : EntityList<Guest>())
|
||
{
|
||
if (peep->State != PeepState::Queuing)
|
||
continue;
|
||
if (peep->CurrentRide != id)
|
||
continue;
|
||
|
||
peep->RemoveFromQueue();
|
||
peep->SetState(PeepState::Falling);
|
||
}
|
||
}
|
||
|
||
RideMode Ride::GetDefaultMode() const
|
||
{
|
||
return GetRideTypeDescriptor().DefaultMode;
|
||
}
|
||
|
||
static bool RideTypeWithTrackColoursExists(ride_type_t rideType, const TrackColour& colours)
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.type != rideType)
|
||
continue;
|
||
if (ride.track_colour[0].main != colours.main)
|
||
continue;
|
||
if (ride.track_colour[0].additional != colours.additional)
|
||
continue;
|
||
if (ride.track_colour[0].supports != colours.supports)
|
||
continue;
|
||
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool Ride::NameExists(std::string_view name, RideId excludeRideId)
|
||
{
|
||
char buffer[256]{};
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.id != excludeRideId)
|
||
{
|
||
Formatter ft;
|
||
ride.FormatNameTo(ft);
|
||
FormatStringLegacy(buffer, 256, STR_STRINGID, ft.Data());
|
||
if (name == buffer && RideHasAnyTrackElements(ride))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
int32_t RideGetRandomColourPresetIndex(ride_type_t rideType)
|
||
{
|
||
if (rideType >= std::size(RideTypeDescriptors))
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
// Find all the presets that haven't yet been used in the park for this ride type
|
||
const auto& colourPresets = GetRideTypeDescriptor(rideType).ColourPresets;
|
||
std::vector<uint8_t> unused;
|
||
unused.reserve(colourPresets.count);
|
||
for (uint8_t i = 0; i < colourPresets.count; i++)
|
||
{
|
||
const auto& colours = colourPresets.list[i];
|
||
if (!RideTypeWithTrackColoursExists(rideType, colours))
|
||
{
|
||
unused.push_back(static_cast<uint8_t>(i));
|
||
}
|
||
}
|
||
|
||
// If all presets have been used, just go with a random preset
|
||
if (unused.size() == 0)
|
||
return UtilRand() % colourPresets.count;
|
||
|
||
// Choose a random preset from the list of unused presets
|
||
auto unusedIndex = UtilRand() % unused.size();
|
||
return unused[unusedIndex];
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Based on rct2: 0x006B4776
|
||
*/
|
||
void Ride::SetColourPreset(uint8_t index)
|
||
{
|
||
const TrackColourPresetList* colourPresets = &GetRideTypeDescriptor().ColourPresets;
|
||
TrackColour colours = { COLOUR_BLACK, COLOUR_BLACK, COLOUR_BLACK };
|
||
// Stalls save their default colour in the vehicle settings (since they share a common ride type)
|
||
if (!IsRide())
|
||
{
|
||
const auto* rideEntry = GetRideEntryByIndex(subtype);
|
||
if (rideEntry != nullptr && rideEntry->vehicle_preset_list->count > 0)
|
||
{
|
||
auto list = rideEntry->vehicle_preset_list->list[0];
|
||
colours = { list.Body, list.Trim, list.Tertiary };
|
||
}
|
||
}
|
||
else if (index < colourPresets->count)
|
||
{
|
||
colours = colourPresets->list[index];
|
||
}
|
||
for (int32_t i = 0; i < OpenRCT2::Limits::NumColourSchemes; i++)
|
||
{
|
||
track_colour[i].main = colours.main;
|
||
track_colour[i].additional = colours.additional;
|
||
track_colour[i].supports = colours.supports;
|
||
}
|
||
colour_scheme_type = 0;
|
||
}
|
||
|
||
money64 RideGetCommonPrice(const Ride& forRide)
|
||
{
|
||
for (const auto& ride : GetRideManager())
|
||
{
|
||
if (ride.type == forRide.type && ride.id != forRide.id)
|
||
{
|
||
return ride.price[0];
|
||
}
|
||
}
|
||
|
||
return kMoney64Undefined;
|
||
}
|
||
|
||
void Ride::SetNameToDefault()
|
||
{
|
||
char rideNameBuffer[256]{};
|
||
|
||
// Increment default name number until we find a unique name
|
||
custom_name = {};
|
||
default_name_number = 0;
|
||
do
|
||
{
|
||
default_name_number++;
|
||
Formatter ft;
|
||
FormatNameTo(ft);
|
||
FormatStringLegacy(rideNameBuffer, 256, STR_STRINGID, ft.Data());
|
||
} while (Ride::NameExists(rideNameBuffer, id));
|
||
}
|
||
|
||
/**
|
||
* This will return the name of the ride, as seen in the New Ride window.
|
||
*/
|
||
RideNaming GetRideNaming(const ride_type_t rideType, const RideObjectEntry& rideEntry)
|
||
{
|
||
const auto& rtd = GetRideTypeDescriptor(rideType);
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
|
||
{
|
||
return rtd.Naming;
|
||
}
|
||
|
||
return rideEntry.naming;
|
||
}
|
||
|
||
/*
|
||
* The next eight functions are helpers to access ride data at the offset 10E &
|
||
* 110. Known as the turn counts. There are 3 different types (default, banked, sloped)
|
||
* and there are 4 counts as follows:
|
||
*
|
||
* 1 element turns: low 5 bits
|
||
* 2 element turns: bits 6-8
|
||
* 3 element turns: bits 9-11
|
||
* 4 element or more turns: bits 12-15
|
||
*
|
||
* 4 plus elements only possible on sloped type. Falls back to 3 element
|
||
* if by some miracle you manage 4 element none sloped.
|
||
*/
|
||
|
||
void IncrementTurnCount1Element(Ride& ride, uint8_t type)
|
||
{
|
||
uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
turn_count = &ride.turn_count_default;
|
||
break;
|
||
case 1:
|
||
turn_count = &ride.turn_count_banked;
|
||
break;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
uint16_t value = (*turn_count & kTurnMask1Element) + 1;
|
||
*turn_count &= ~kTurnMask1Element;
|
||
|
||
if (value > kTurnMask1Element)
|
||
value = kTurnMask1Element;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
void IncrementTurnCount2Elements(Ride& ride, uint8_t type)
|
||
{
|
||
uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
turn_count = &ride.turn_count_default;
|
||
break;
|
||
case 1:
|
||
turn_count = &ride.turn_count_banked;
|
||
break;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
uint16_t value = (*turn_count & kTurnMask2Elements) + 0x20;
|
||
*turn_count &= ~kTurnMask2Elements;
|
||
|
||
if (value > kTurnMask2Elements)
|
||
value = kTurnMask2Elements;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
void IncrementTurnCount3Elements(Ride& ride, uint8_t type)
|
||
{
|
||
uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
turn_count = &ride.turn_count_default;
|
||
break;
|
||
case 1:
|
||
turn_count = &ride.turn_count_banked;
|
||
break;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
uint16_t value = (*turn_count & kTurnMask3Elements) + 0x100;
|
||
*turn_count &= ~kTurnMask3Elements;
|
||
|
||
if (value > kTurnMask3Elements)
|
||
value = kTurnMask3Elements;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
void IncrementTurnCount4PlusElements(Ride& ride, uint8_t type)
|
||
{
|
||
uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
case 1:
|
||
// Just in case fallback to 3 element turn
|
||
IncrementTurnCount3Elements(ride, type);
|
||
return;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
uint16_t value = (*turn_count & kTurnMask4PlusElements) + 0x800;
|
||
*turn_count &= ~kTurnMask4PlusElements;
|
||
|
||
if (value > kTurnMask4PlusElements)
|
||
value = kTurnMask4PlusElements;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
int32_t GetTurnCount1Element(const Ride& ride, uint8_t type)
|
||
{
|
||
const uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
turn_count = &ride.turn_count_default;
|
||
break;
|
||
case 1:
|
||
turn_count = &ride.turn_count_banked;
|
||
break;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return 0;
|
||
}
|
||
|
||
return (*turn_count) & kTurnMask1Element;
|
||
}
|
||
|
||
int32_t GetTurnCount2Elements(const Ride& ride, uint8_t type)
|
||
{
|
||
const uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
turn_count = &ride.turn_count_default;
|
||
break;
|
||
case 1:
|
||
turn_count = &ride.turn_count_banked;
|
||
break;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return 0;
|
||
}
|
||
|
||
return ((*turn_count) & kTurnMask2Elements) >> 5;
|
||
}
|
||
|
||
int32_t GetTurnCount3Elements(const Ride& ride, uint8_t type)
|
||
{
|
||
const uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
turn_count = &ride.turn_count_default;
|
||
break;
|
||
case 1:
|
||
turn_count = &ride.turn_count_banked;
|
||
break;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return 0;
|
||
}
|
||
|
||
return ((*turn_count) & kTurnMask3Elements) >> 8;
|
||
}
|
||
|
||
int32_t GetTurnCount4PlusElements(const Ride& ride, uint8_t type)
|
||
{
|
||
const uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
case 1:
|
||
return 0;
|
||
case 2:
|
||
turn_count = &ride.turn_count_sloped;
|
||
break;
|
||
default:
|
||
return 0;
|
||
}
|
||
|
||
return ((*turn_count) & kTurnMask4PlusElements) >> 11;
|
||
}
|
||
|
||
bool Ride::HasSpinningTunnel() const
|
||
{
|
||
return special_track_elements & RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS;
|
||
}
|
||
|
||
bool Ride::HasWaterSplash() const
|
||
{
|
||
return special_track_elements & RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS;
|
||
}
|
||
|
||
bool Ride::HasRapids() const
|
||
{
|
||
return special_track_elements & RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS;
|
||
}
|
||
|
||
bool Ride::HasLogReverser() const
|
||
{
|
||
return special_track_elements & RIDE_ELEMENT_REVERSER_OR_WATERFALL;
|
||
}
|
||
|
||
bool Ride::HasWaterfall() const
|
||
{
|
||
return special_track_elements & RIDE_ELEMENT_REVERSER_OR_WATERFALL;
|
||
}
|
||
|
||
bool Ride::HasWhirlpool() const
|
||
{
|
||
return special_track_elements & RIDE_ELEMENT_WHIRLPOOL;
|
||
}
|
||
|
||
uint8_t RideGetHelixSections(const Ride& ride)
|
||
{
|
||
// Helix sections stored in the low 5 bits.
|
||
return ride.special_track_elements & 0x1F;
|
||
}
|
||
|
||
bool Ride::IsPoweredLaunched() const
|
||
{
|
||
return mode == RideMode::PoweredLaunchPasstrough || mode == RideMode::PoweredLaunch
|
||
|| mode == RideMode::PoweredLaunchBlockSectioned;
|
||
}
|
||
|
||
bool Ride::IsBlockSectioned() const
|
||
{
|
||
return mode == RideMode::ContinuousCircuitBlockSectioned || mode == RideMode::PoweredLaunchBlockSectioned;
|
||
}
|
||
|
||
bool RideHasAnyTrackElements(const Ride& ride)
|
||
{
|
||
TileElementIterator it;
|
||
|
||
TileElementIteratorBegin(&it);
|
||
while (TileElementIteratorNext(&it))
|
||
{
|
||
if (it.element->GetType() != TileElementType::Track)
|
||
continue;
|
||
if (it.element->AsTrack()->GetRideIndex() != ride.id)
|
||
continue;
|
||
if (it.element->IsGhost())
|
||
continue;
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B59C6
|
||
*/
|
||
void InvalidateTestResults(Ride& ride)
|
||
{
|
||
ride.measurement = {};
|
||
ride.excitement = kRideRatingUndefined;
|
||
ride.lifecycle_flags &= ~RIDE_LIFECYCLE_TESTED;
|
||
ride.lifecycle_flags &= ~RIDE_LIFECYCLE_TEST_IN_PROGRESS;
|
||
if (ride.lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK)
|
||
{
|
||
for (int32_t i = 0; i < ride.NumTrains; i++)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(ride.vehicles[i]);
|
||
if (vehicle != nullptr)
|
||
{
|
||
vehicle->ClearFlag(VehicleFlags::Testing);
|
||
}
|
||
}
|
||
}
|
||
WindowInvalidateByNumber(WindowClass::Ride, ride.id.ToUnderlying());
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7481
|
||
*
|
||
* @param rideIndex (dl)
|
||
* @param reliabilityIncreaseFactor (ax)
|
||
*/
|
||
void RideFixBreakdown(Ride& ride, int32_t reliabilityIncreaseFactor)
|
||
{
|
||
ride.lifecycle_flags &= ~RIDE_LIFECYCLE_BREAKDOWN_PENDING;
|
||
ride.lifecycle_flags &= ~RIDE_LIFECYCLE_BROKEN_DOWN;
|
||
ride.lifecycle_flags &= ~RIDE_LIFECYCLE_DUE_INSPECTION;
|
||
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST | RIDE_INVALIDATE_RIDE_MAINTENANCE;
|
||
|
||
if (ride.lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK)
|
||
{
|
||
for (int32_t i = 0; i < ride.NumTrains; i++)
|
||
{
|
||
for (Vehicle* vehicle = GetEntity<Vehicle>(ride.vehicles[i]); vehicle != nullptr;
|
||
vehicle = GetEntity<Vehicle>(vehicle->next_vehicle_on_train))
|
||
{
|
||
vehicle->ClearFlag(VehicleFlags::StoppedOnLift);
|
||
vehicle->ClearFlag(VehicleFlags::CarIsBroken);
|
||
vehicle->ClearFlag(VehicleFlags::TrainIsBroken);
|
||
}
|
||
}
|
||
}
|
||
|
||
uint8_t unreliability = 100 - ride.reliability_percentage;
|
||
ride.reliability += reliabilityIncreaseFactor * (unreliability / 2);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DE102
|
||
*/
|
||
void RideUpdateVehicleColours(const Ride& ride)
|
||
{
|
||
if (ride.type == RIDE_TYPE_SPACE_RINGS || ride.GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_VEHICLE_IS_INTEGRAL))
|
||
{
|
||
GfxInvalidateScreen();
|
||
}
|
||
|
||
for (int32_t i = 0; i <= OpenRCT2::Limits::MaxTrainsPerRide; i++)
|
||
{
|
||
int32_t carIndex = 0;
|
||
VehicleColour colours = {};
|
||
|
||
for (Vehicle* vehicle = GetEntity<Vehicle>(ride.vehicles[i]); vehicle != nullptr;
|
||
vehicle = GetEntity<Vehicle>(vehicle->next_vehicle_on_train))
|
||
{
|
||
switch (ride.colour_scheme_type & 3)
|
||
{
|
||
case RIDE_COLOUR_SCHEME_MODE_ALL_SAME:
|
||
colours = ride.vehicle_colours[0];
|
||
break;
|
||
case RIDE_COLOUR_SCHEME_MODE_DIFFERENT_PER_TRAIN:
|
||
colours = ride.vehicle_colours[i];
|
||
break;
|
||
case RIDE_COLOUR_SCHEME_MODE_DIFFERENT_PER_CAR:
|
||
if (vehicle->HasFlag(VehicleFlags::CarIsReversed))
|
||
{
|
||
colours = ride.vehicle_colours[std::min(
|
||
(ride.num_cars_per_train - 1) - carIndex, OpenRCT2::Limits::MaxCarsPerTrain - 1)];
|
||
}
|
||
else
|
||
{
|
||
colours = ride.vehicle_colours[std::min(carIndex, OpenRCT2::Limits::MaxCarsPerTrain - 1)];
|
||
}
|
||
break;
|
||
}
|
||
|
||
vehicle->colours = colours;
|
||
vehicle->Invalidate();
|
||
carIndex++;
|
||
}
|
||
}
|
||
}
|
||
|
||
uint8_t RideEntryGetVehicleAtPosition(int32_t rideEntryIndex, int32_t numCarsPerTrain, int32_t position)
|
||
{
|
||
const auto* rideEntry = GetRideEntryByIndex(rideEntryIndex);
|
||
if (position == 0 && rideEntry->FrontCar != 255)
|
||
{
|
||
return rideEntry->FrontCar;
|
||
}
|
||
if (position == 1 && rideEntry->SecondCar != 255)
|
||
{
|
||
return rideEntry->SecondCar;
|
||
}
|
||
if (position == 2 && rideEntry->ThirdCar != 255)
|
||
{
|
||
return rideEntry->ThirdCar;
|
||
}
|
||
if (position == numCarsPerTrain - 1 && rideEntry->RearCar != 255)
|
||
{
|
||
return rideEntry->RearCar;
|
||
}
|
||
|
||
return rideEntry->DefaultCar;
|
||
}
|
||
|
||
using namespace OpenRCT2::Entity::Yaw;
|
||
|
||
struct NecessarySpriteGroup
|
||
{
|
||
SpriteGroupType VehicleSpriteGroup;
|
||
SpritePrecision MinPrecision;
|
||
};
|
||
|
||
// Finds track pieces that a given ride entry has sprites for
|
||
OpenRCT2::BitSet<TRACK_GROUP_COUNT> RideEntryGetSupportedTrackPieces(const RideObjectEntry& rideEntry)
|
||
{
|
||
// TODO: Use a std::span when C++20 available as 6 is due to jagged array
|
||
static const std::array<NecessarySpriteGroup, 6> trackPieceRequiredSprites[] = {
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::None }, // TRACK_FLAT
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_STRAIGHT
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_STATION_END
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4 }, // TRACK_LIFT_HILL
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60,
|
||
SpritePrecision::Sprites4 }, // TRACK_LIFT_HILL_STEEP
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites16 }, // TRACK_LIFT_HILL_CURVE
|
||
{ SpriteGroupType::FlatBanked22, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked45,
|
||
SpritePrecision::Sprites16 }, // TRACK_FLAT_ROLL_BANKING
|
||
{ SpriteGroupType::Slopes60, SpritePrecision::Sprites4, SpriteGroupType::Slopes75, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes90, SpritePrecision::Sprites4, SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopeInverted, SpritePrecision::Sprites4 }, // TRACK_VERTICAL_LOOP
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4 }, // TRACK_SLOPE
|
||
{ SpriteGroupType::Slopes60, SpritePrecision::Sprites4 }, // TRACK_SLOPE_STEEP_DOWN
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60,
|
||
SpritePrecision::Sprites4 }, // TRACK_SLOPE_LONG
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites16 }, // TRACK_SLOPE_CURVE
|
||
{ SpriteGroupType::Slopes60, SpritePrecision::Sprites16 }, // TRACK_SLOPE_CURVE_STEEP
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_S_BEND
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_CURVE_VERY_SMALL
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_CURVE_SMALL
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_CURVE
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_CURVE_LARGE
|
||
{ SpriteGroupType::FlatBanked22, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked45, SpritePrecision::Sprites4,
|
||
SpriteGroupType::FlatBanked67, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked90, SpritePrecision::Sprites4,
|
||
SpriteGroupType::InlineTwists, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_TWIST
|
||
{ SpriteGroupType::Slopes60, SpritePrecision::Sprites4, SpriteGroupType::Slopes75, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes90, SpritePrecision::Sprites4, SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopeInverted, SpritePrecision::Sprites4 }, // TRACK_HALF_LOOP
|
||
{ SpriteGroupType::Corkscrews, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_CORKSCREW
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::None }, // TRACK_TOWER_BASE
|
||
{ SpriteGroupType::FlatBanked45, SpritePrecision::Sprites16 }, // TRACK_HELIX_UP_BANKED_HALF
|
||
{ SpriteGroupType::FlatBanked45, SpritePrecision::Sprites16 }, // TRACK_HELIX_DOWN_BANKED_HALF
|
||
{ SpriteGroupType::FlatBanked45, SpritePrecision::Sprites16 }, // TRACK_HELIX_UP_BANKED_QUARTER
|
||
{ SpriteGroupType::FlatBanked45, SpritePrecision::Sprites16 }, // TRACK_HELIX_DOWN_BANKED_QUARTER
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_HELIX_UP_UNBANKED_QUARTER
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_HELIX_DOWN_UNBANKED_QUARTER
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_BRAKES
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_ON_RIDE_PHOTO
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4, SpriteGroupType::Slopes12,
|
||
SpritePrecision::Sprites4 }, // TRACK_WATER_SPLASH
|
||
{ SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_SLOPE_VERTICAL
|
||
{ SpriteGroupType::FlatBanked22, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked45, SpritePrecision::Sprites4,
|
||
SpriteGroupType::InlineTwists, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_BARREL_ROLL
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4 }, // TRACK_POWERED_LIFT
|
||
{ SpriteGroupType::Slopes60, SpritePrecision::Sprites4, SpriteGroupType::Slopes75, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes90, SpritePrecision::Sprites4, SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopeInverted, SpritePrecision::Sprites4 }, // TRACK_HALF_LOOP_LARGE
|
||
{ SpriteGroupType::Slopes12Banked22, SpritePrecision::Sprites16 }, // TRACK_SLOPE_CURVE_BANKED
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_LOG_FLUME_REVERSER
|
||
{ SpriteGroupType::FlatBanked22, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked45, SpritePrecision::Sprites4,
|
||
SpriteGroupType::InlineTwists, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_HEARTLINE_ROLL
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites16 }, // TRACK_REVERSER
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4, SpriteGroupType::Slopes25, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes60, SpritePrecision::Sprites4, SpriteGroupType::Slopes75, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes90, SpritePrecision::Sprites4 }, // TRACK_REVERSE_FREEFALL
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4, SpriteGroupType::Slopes25, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes60, SpritePrecision::Sprites4, SpriteGroupType::Slopes75, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes90, SpritePrecision::Sprites4 }, // TRACK_SLOPE_TO_FLAT
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_BLOCK_BRAKES
|
||
{ SpriteGroupType::Slopes25Banked22, SpritePrecision::Sprites4 }, // TRACK_SLOPE_ROLL_BANKING
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60,
|
||
SpritePrecision::Sprites4 }, // TRACK_SLOPE_STEEP_LONG
|
||
{ SpriteGroupType::Slopes90, SpritePrecision::Sprites16 }, // TRACK_CURVE_VERTICAL
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60,
|
||
SpritePrecision::Sprites4 }, // TRACK_LIFT_HILL_CABLE
|
||
{ SpriteGroupType::CurvedLiftHillUp, SpritePrecision::Sprites16 }, // TRACK_LIFT_HILL_CURVED
|
||
{ SpriteGroupType::Slopes90, SpritePrecision::Sprites4, SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopeInverted, SpritePrecision::Sprites4 }, // TRACK_QUARTER_LOOP
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_SPINNING_TUNNEL
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_BOOSTER
|
||
{ SpriteGroupType::FlatBanked22, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked45, SpritePrecision::Sprites4,
|
||
SpriteGroupType::FlatBanked67, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked90, SpritePrecision::Sprites4,
|
||
SpriteGroupType::InlineTwists, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_INLINE_TWIST_UNINVERTED
|
||
{ SpriteGroupType::FlatBanked22, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked45, SpritePrecision::Sprites4,
|
||
SpriteGroupType::FlatBanked67, SpritePrecision::Sprites4, SpriteGroupType::FlatBanked90, SpritePrecision::Sprites4,
|
||
SpriteGroupType::InlineTwists, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_INLINE_TWIST_INVERTED
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_QUARTER_LOOP_UNINVERTED_UP
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_QUARTER_LOOP_UNINVERTED_DOWN
|
||
{ SpriteGroupType::Slopes90, SpritePrecision::Sprites4, SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopeInverted, SpritePrecision::Sprites4 }, // TRACK_QUARTER_LOOP_INVERTED_UP
|
||
{ SpriteGroupType::Slopes90, SpritePrecision::Sprites4, SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopeInverted, SpritePrecision::Sprites4 }, // TRACK_QUARTER_LOOP_INVERTED_DOWN
|
||
{ SpriteGroupType::Slopes12, SpritePrecision::Sprites4 }, // TRACK_RAPIDS
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_HALF_LOOP_UNINVERTED_UP
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_HALF_LOOP_INVERTED_DOWN
|
||
{}, // TRACK_FLAT_RIDE_BASE
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_WATERFALL
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_WHIRLPOOL
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60,
|
||
SpritePrecision::Sprites4 }, // TRACK_BRAKE_FOR_DROP
|
||
{ SpriteGroupType::Corkscrews, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_CORKSCREW_UNINVERTED
|
||
{ SpriteGroupType::Corkscrews, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_CORKSCREW_INVERTED
|
||
{ SpriteGroupType::Slopes12, SpritePrecision::Sprites4, SpriteGroupType::Slopes25,
|
||
SpritePrecision::Sprites4 }, // TRACK_HEARTLINE_TRANSFER
|
||
{}, // TRACK_MINI_GOLF_HOLE
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites4 }, // TRACK_ROTATION_CONTROL_TOGGLE
|
||
{ SpriteGroupType::Slopes60, SpritePrecision::Sprites4 }, // TRACK_SLOPE_STEEP_UP
|
||
{}, // TRACK_CORKSCREW_LARGE
|
||
{}, // TRACK_HALF_LOOP_MEDIUM
|
||
{}, // TRACK_ZERO_G_ROLL
|
||
{}, // TRACK_ZERO_G_ROLL_LARGE
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_LARGE_HALF_LOOP_UNINVERTED_UP
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_LARGE_HALF_LOOP_INVERTED_DOWN
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90, SpritePrecision::Sprites4,
|
||
SpriteGroupType::SlopesLoop, SpritePrecision::Sprites4, SpriteGroupType::SlopeInverted,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_LARGE_HALF_LOOP_UNINVERTED_DOWN
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_LARGE_HALF_LOOP_INVERTED_UP
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_HALF_LOOP_INVERTED_UP
|
||
{ SpriteGroupType::Slopes25, SpritePrecision::Sprites4, SpriteGroupType::Slopes60, SpritePrecision::Sprites4,
|
||
SpriteGroupType::Slopes75, SpritePrecision::Sprites4, SpriteGroupType::Slopes90,
|
||
SpritePrecision::Sprites4 }, // TRACK_FLYING_HALF_LOOP_UNINVERTED_DOWN
|
||
{}, // TRACK_SLOPE_CURVE_LARGE
|
||
{}, // TRACK_SLOPE_CURVE_LARGE_BANKED
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites8 }, // TRACK_DIAG_BRAKES
|
||
{ SpriteGroupType::SlopeFlat, SpritePrecision::Sprites8 }, // TRACK_DIAG_BLOCK_BRAKES
|
||
};
|
||
static_assert(std::size(trackPieceRequiredSprites) == TRACK_GROUP_COUNT);
|
||
|
||
// Only check default vehicle; it's assumed the others will have correct sprites if this one does (I've yet to find an
|
||
// exception, at least)
|
||
auto supportedPieces = OpenRCT2::BitSet<TRACK_GROUP_COUNT>();
|
||
supportedPieces.flip();
|
||
auto defaultVehicle = rideEntry.GetDefaultCar();
|
||
if (defaultVehicle != nullptr)
|
||
{
|
||
for (size_t i = 0; i < std::size(trackPieceRequiredSprites); i++)
|
||
{
|
||
for (auto& group : trackPieceRequiredSprites[i])
|
||
{
|
||
auto precision = defaultVehicle->SpriteGroups[EnumValue(group.VehicleSpriteGroup)].spritePrecision;
|
||
if (precision < group.MinPrecision)
|
||
supportedPieces.set(i, false);
|
||
}
|
||
}
|
||
}
|
||
return supportedPieces;
|
||
}
|
||
|
||
static std::optional<int32_t> RideGetSmallestStationLength(const Ride& ride)
|
||
{
|
||
std::optional<int32_t> result;
|
||
for (const auto& station : ride.GetStations())
|
||
{
|
||
if (!station.Start.IsNull())
|
||
{
|
||
if (!result.has_value() || station.Length < result.value())
|
||
{
|
||
result = station.Length;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006CB3AA
|
||
*/
|
||
static int32_t RideGetTrackLength(const Ride& ride)
|
||
{
|
||
TileElement* tileElement = nullptr;
|
||
track_type_t trackType;
|
||
CoordsXYZ trackStart;
|
||
bool foundTrack = false;
|
||
|
||
for (const auto& station : ride.GetStations())
|
||
{
|
||
trackStart = station.GetStart();
|
||
if (trackStart.IsNull())
|
||
continue;
|
||
|
||
tileElement = MapGetFirstElementAt(trackStart);
|
||
if (tileElement == nullptr)
|
||
continue;
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TileElementType::Track)
|
||
continue;
|
||
|
||
trackType = tileElement->AsTrack()->GetTrackType();
|
||
const auto& ted = GetTrackElementDescriptor(trackType);
|
||
if (!(std::get<0>(ted.SequenceProperties) & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
continue;
|
||
|
||
if (tileElement->GetBaseZ() != trackStart.z)
|
||
continue;
|
||
|
||
foundTrack = true;
|
||
} while (!foundTrack && !(tileElement++)->IsLastForTile());
|
||
|
||
if (foundTrack)
|
||
break;
|
||
}
|
||
|
||
if (!foundTrack)
|
||
return 0;
|
||
|
||
RideId rideIndex = tileElement->AsTrack()->GetRideIndex();
|
||
|
||
WindowBase* w = WindowFindByClass(WindowClass::RideConstruction);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && _currentRideIndex == rideIndex)
|
||
{
|
||
RideConstructionInvalidateCurrentTrack();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
int32_t result = 0;
|
||
|
||
TrackCircuitIterator it;
|
||
TrackCircuitIteratorBegin(&it, { trackStart.x, trackStart.y, tileElement });
|
||
|
||
TrackCircuitIterator slowIt = it;
|
||
while (TrackCircuitIteratorNext(&it))
|
||
{
|
||
trackType = it.current.element->AsTrack()->GetTrackType();
|
||
const auto& ted = GetTrackElementDescriptor(trackType);
|
||
result += ted.PieceLength;
|
||
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
TrackCircuitIteratorNext(&slowIt);
|
||
if (TrackCircuitIteratorsMatch(&it, &slowIt))
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DD57D
|
||
*/
|
||
void Ride::UpdateMaxVehicles()
|
||
{
|
||
if (subtype == OBJECT_ENTRY_INDEX_NULL)
|
||
return;
|
||
|
||
const auto* rideEntry = GetRideEntryByIndex(subtype);
|
||
if (rideEntry == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
uint8_t numCarsPerTrain, numTrains;
|
||
int32_t maxNumTrains;
|
||
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (rideEntry->cars_per_flat_ride == NoFlatRideCars)
|
||
{
|
||
num_cars_per_train = std::max(rideEntry->min_cars_in_train, num_cars_per_train);
|
||
MinCarsPerTrain = rideEntry->min_cars_in_train;
|
||
MaxCarsPerTrain = rideEntry->max_cars_in_train;
|
||
|
||
// Calculate maximum train length based on smallest station length
|
||
auto stationNumTiles = RideGetSmallestStationLength(*this);
|
||
if (!stationNumTiles.has_value())
|
||
return;
|
||
|
||
auto stationLength = (stationNumTiles.value() * 0x44180) - 0x16B2A;
|
||
int32_t maxMass = rtd.MaxMass << 8;
|
||
int32_t maxCarsPerTrain = 1;
|
||
for (int32_t numCars = rideEntry->max_cars_in_train; numCars > 0; numCars--)
|
||
{
|
||
int32_t trainLength = 0;
|
||
int32_t totalMass = 0;
|
||
for (int32_t i = 0; i < numCars; i++)
|
||
{
|
||
const auto& carEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, numCars, i)];
|
||
trainLength += carEntry.spacing;
|
||
totalMass += carEntry.car_mass;
|
||
}
|
||
|
||
if (trainLength <= stationLength && totalMass <= maxMass)
|
||
{
|
||
maxCarsPerTrain = numCars;
|
||
break;
|
||
}
|
||
}
|
||
int32_t newCarsPerTrain = std::max(proposed_num_cars_per_train, rideEntry->min_cars_in_train);
|
||
maxCarsPerTrain = std::max(maxCarsPerTrain, static_cast<int32_t>(rideEntry->min_cars_in_train));
|
||
if (!GetGameState().Cheats.DisableTrainLengthLimit)
|
||
{
|
||
newCarsPerTrain = std::min(maxCarsPerTrain, newCarsPerTrain);
|
||
}
|
||
MaxCarsPerTrain = maxCarsPerTrain;
|
||
MinCarsPerTrain = rideEntry->min_cars_in_train;
|
||
|
||
switch (mode)
|
||
{
|
||
case RideMode::ContinuousCircuitBlockSectioned:
|
||
case RideMode::PoweredLaunchBlockSectioned:
|
||
maxNumTrains = std::clamp<int32_t>(num_stations + num_block_brakes - 1, 1, OpenRCT2::Limits::MaxTrainsPerRide);
|
||
break;
|
||
case RideMode::ReverseInclineLaunchedShuttle:
|
||
case RideMode::PoweredLaunchPasstrough:
|
||
case RideMode::Shuttle:
|
||
case RideMode::LimPoweredLaunch:
|
||
case RideMode::PoweredLaunch:
|
||
maxNumTrains = 1;
|
||
break;
|
||
default:
|
||
// Calculate maximum number of trains
|
||
int32_t trainLength = 0;
|
||
for (int32_t i = 0; i < newCarsPerTrain; i++)
|
||
{
|
||
const auto& carEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, newCarsPerTrain, i)];
|
||
trainLength += carEntry.spacing;
|
||
}
|
||
|
||
int32_t totalLength = trainLength / 2;
|
||
if (newCarsPerTrain != 1)
|
||
totalLength /= 2;
|
||
|
||
maxNumTrains = 0;
|
||
do
|
||
{
|
||
maxNumTrains++;
|
||
totalLength += trainLength;
|
||
} while (totalLength <= stationLength);
|
||
|
||
if ((mode != RideMode::StationToStation && mode != RideMode::ContinuousCircuit)
|
||
|| !(rtd.HasFlag(RIDE_TYPE_FLAG_ALLOW_MORE_VEHICLES_THAN_STATION_FITS)))
|
||
{
|
||
maxNumTrains = std::min(maxNumTrains, int32_t(OpenRCT2::Limits::MaxTrainsPerRide));
|
||
}
|
||
else
|
||
{
|
||
const auto& firstCarEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, newCarsPerTrain, 0)];
|
||
int32_t poweredMaxSpeed = firstCarEntry.powered_max_speed;
|
||
|
||
int32_t totalSpacing = 0;
|
||
for (int32_t i = 0; i < newCarsPerTrain; i++)
|
||
{
|
||
const auto& carEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, newCarsPerTrain, i)];
|
||
totalSpacing += carEntry.spacing;
|
||
}
|
||
|
||
totalSpacing >>= 13;
|
||
int32_t trackLength = RideGetTrackLength(*this) / 4;
|
||
if (poweredMaxSpeed > 10)
|
||
trackLength = (trackLength * 3) / 4;
|
||
if (poweredMaxSpeed > 25)
|
||
trackLength = (trackLength * 3) / 4;
|
||
if (poweredMaxSpeed > 40)
|
||
trackLength = (trackLength * 3) / 4;
|
||
|
||
maxNumTrains = 0;
|
||
int32_t length = 0;
|
||
do
|
||
{
|
||
maxNumTrains++;
|
||
length += totalSpacing;
|
||
} while (maxNumTrains < OpenRCT2::Limits::MaxTrainsPerRide && length < trackLength);
|
||
}
|
||
break;
|
||
}
|
||
max_trains = maxNumTrains;
|
||
|
||
numCarsPerTrain = std::min(proposed_num_cars_per_train, static_cast<uint8_t>(newCarsPerTrain));
|
||
}
|
||
else
|
||
{
|
||
max_trains = rideEntry->cars_per_flat_ride;
|
||
MinCarsPerTrain = rideEntry->min_cars_in_train;
|
||
MaxCarsPerTrain = rideEntry->max_cars_in_train;
|
||
numCarsPerTrain = rideEntry->max_cars_in_train;
|
||
maxNumTrains = rideEntry->cars_per_flat_ride;
|
||
}
|
||
|
||
if (GetGameState().Cheats.DisableTrainLengthLimit)
|
||
{
|
||
maxNumTrains = OpenRCT2::Limits::MaxTrainsPerRide;
|
||
}
|
||
numTrains = std::min(ProposedNumTrains, static_cast<uint8_t>(maxNumTrains));
|
||
|
||
// Refresh new current num vehicles / num cars per vehicle
|
||
if (numTrains != NumTrains || numCarsPerTrain != num_cars_per_train)
|
||
{
|
||
num_cars_per_train = numCarsPerTrain;
|
||
NumTrains = numTrains;
|
||
WindowInvalidateByNumber(WindowClass::Ride, id.ToUnderlying());
|
||
}
|
||
}
|
||
|
||
void Ride::UpdateNumberOfCircuits()
|
||
{
|
||
if (!CanHaveMultipleCircuits())
|
||
{
|
||
num_circuits = 1;
|
||
}
|
||
}
|
||
|
||
void Ride::SetRideEntry(ObjectEntryIndex entryIndex)
|
||
{
|
||
auto colour = RideGetUnusedPresetVehicleColour(entryIndex);
|
||
auto rideSetVehicleAction = RideSetVehicleAction(id, RideSetVehicleType::RideEntry, entryIndex, colour);
|
||
GameActions::Execute(&rideSetVehicleAction);
|
||
}
|
||
|
||
void Ride::SetNumTrains(int32_t numTrains)
|
||
{
|
||
auto rideSetVehicleAction = RideSetVehicleAction(id, RideSetVehicleType::NumTrains, numTrains);
|
||
GameActions::Execute(&rideSetVehicleAction);
|
||
}
|
||
|
||
void Ride::SetNumCarsPerVehicle(int32_t numCarsPerVehicle)
|
||
{
|
||
auto rideSetVehicleAction = RideSetVehicleAction(id, RideSetVehicleType::NumCarsPerTrain, numCarsPerVehicle);
|
||
GameActions::Execute(&rideSetVehicleAction);
|
||
}
|
||
|
||
void Ride::SetReversedTrains(bool reverseTrains)
|
||
{
|
||
auto rideSetVehicleAction = RideSetVehicleAction(id, RideSetVehicleType::TrainsReversed, reverseTrains);
|
||
GameActions::Execute(&rideSetVehicleAction);
|
||
}
|
||
|
||
void Ride::SetToDefaultInspectionInterval()
|
||
{
|
||
uint8_t defaultInspectionInterval = gConfigGeneral.DefaultInspectionInterval;
|
||
if (inspection_interval != defaultInspectionInterval)
|
||
{
|
||
if (defaultInspectionInterval <= RIDE_INSPECTION_NEVER)
|
||
{
|
||
SetOperatingSetting(id, RideSetSetting::InspectionInterval, defaultInspectionInterval);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B752C
|
||
*/
|
||
void Ride::Crash(uint8_t vehicleIndex)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(vehicles[vehicleIndex]);
|
||
|
||
if (!(gScreenFlags & SCREEN_FLAGS_TITLE_DEMO) && vehicle != nullptr)
|
||
{
|
||
// Open ride window for crashed vehicle
|
||
auto intent = Intent(WD_VEHICLE);
|
||
intent.PutExtra(INTENT_EXTRA_VEHICLE, vehicle);
|
||
WindowBase* w = ContextOpenIntent(&intent);
|
||
|
||
Viewport* viewport = WindowGetViewport(w);
|
||
if (w != nullptr && viewport != nullptr)
|
||
{
|
||
viewport->flags |= VIEWPORT_FLAG_SOUND_ON;
|
||
}
|
||
}
|
||
|
||
if (gConfigNotifications.RideCrashed)
|
||
{
|
||
Formatter ft;
|
||
FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_RIDE_HAS_CRASHED, id.ToUnderlying(), ft);
|
||
}
|
||
}
|
||
|
||
// Gets the approximate value of customers per hour for this ride. Multiplies ride_customers_in_last_5_minutes() by 12.
|
||
uint32_t RideCustomersPerHour(const Ride& ride)
|
||
{
|
||
return RideCustomersInLast5Minutes(ride) * 12;
|
||
}
|
||
|
||
// Calculates the number of customers for this ride in the last 5 minutes (or more correctly 9600 game ticks)
|
||
uint32_t RideCustomersInLast5Minutes(const Ride& ride)
|
||
{
|
||
uint32_t sum = 0;
|
||
|
||
for (int32_t i = 0; i < OpenRCT2::Limits::CustomerHistorySize; i++)
|
||
{
|
||
sum += ride.num_customers[i];
|
||
}
|
||
|
||
return sum;
|
||
}
|
||
|
||
Vehicle* RideGetBrokenVehicle(const Ride& ride)
|
||
{
|
||
auto vehicleIndex = ride.vehicles[ride.broken_vehicle];
|
||
Vehicle* vehicle = GetEntity<Vehicle>(vehicleIndex);
|
||
if (vehicle != nullptr)
|
||
{
|
||
return vehicle->GetCar(ride.broken_car);
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006D235B
|
||
*/
|
||
void Ride::Delete()
|
||
{
|
||
RideDelete(id);
|
||
}
|
||
|
||
void Ride::Renew()
|
||
{
|
||
// Set build date to current date (so the ride is brand new)
|
||
build_date = GetDate().GetMonthsElapsed();
|
||
reliability = kRideInitialReliability;
|
||
}
|
||
|
||
RideClassification Ride::GetClassification() const
|
||
{
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
return rtd.Classification;
|
||
}
|
||
|
||
bool Ride::IsRide() const
|
||
{
|
||
return GetClassification() == RideClassification::Ride;
|
||
}
|
||
|
||
money64 RideGetPrice(const Ride& ride)
|
||
{
|
||
if (GetGameState().Park.Flags & PARK_FLAGS_NO_MONEY)
|
||
return 0;
|
||
if (ride.IsRide())
|
||
{
|
||
if (!Park::RidePricesUnlocked())
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
return ride.price[0];
|
||
}
|
||
|
||
/**
|
||
* Return the tile_element of an adjacent station at x,y,z(+-2).
|
||
* Returns nullptr if no suitable tile_element is found.
|
||
*/
|
||
TileElement* GetStationPlatform(const CoordsXYRangedZ& coords)
|
||
{
|
||
bool foundTileElement = false;
|
||
TileElement* tileElement = MapGetFirstElementAt(coords);
|
||
if (tileElement != nullptr)
|
||
{
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TileElementType::Track)
|
||
continue;
|
||
/* Check if tileElement is a station platform. */
|
||
if (!tileElement->AsTrack()->IsStation())
|
||
continue;
|
||
|
||
if (coords.baseZ > tileElement->GetBaseZ() || coords.clearanceZ < tileElement->GetBaseZ())
|
||
{
|
||
/* The base height if tileElement is not within
|
||
* the z tolerance. */
|
||
continue;
|
||
}
|
||
|
||
foundTileElement = true;
|
||
break;
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
if (!foundTileElement)
|
||
{
|
||
return nullptr;
|
||
}
|
||
|
||
return tileElement;
|
||
}
|
||
|
||
/**
|
||
* Check for an adjacent station to x,y,z in direction.
|
||
*/
|
||
static bool CheckForAdjacentStation(const CoordsXYZ& stationCoords, uint8_t direction)
|
||
{
|
||
bool found = false;
|
||
int32_t adjX = stationCoords.x;
|
||
int32_t adjY = stationCoords.y;
|
||
for (uint32_t i = 0; i <= kRideAdjacencyCheckDistance; i++)
|
||
{
|
||
adjX += CoordsDirectionDelta[direction].x;
|
||
adjY += CoordsDirectionDelta[direction].y;
|
||
TileElement* stationElement = GetStationPlatform(
|
||
{ { adjX, adjY, stationCoords.z }, stationCoords.z + 2 * COORDS_Z_STEP });
|
||
if (stationElement != nullptr)
|
||
{
|
||
auto rideIndex = stationElement->AsTrack()->GetRideIndex();
|
||
auto ride = GetRide(rideIndex);
|
||
if (ride != nullptr && (ride->depart_flags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS))
|
||
{
|
||
found = true;
|
||
}
|
||
}
|
||
}
|
||
return found;
|
||
}
|
||
|
||
/**
|
||
* Return whether ride has at least one adjacent station to it.
|
||
*/
|
||
bool RideHasAdjacentStation(const Ride& ride)
|
||
{
|
||
bool found = false;
|
||
|
||
/* Loop through all of the ride stations, checking for an
|
||
* adjacent station on either side. */
|
||
for (const auto& station : ride.GetStations())
|
||
{
|
||
auto stationStart = station.GetStart();
|
||
if (!stationStart.IsNull())
|
||
{
|
||
/* Get the map element for the station start. */
|
||
TileElement* stationElement = GetStationPlatform({ stationStart, stationStart.z + 0 });
|
||
if (stationElement == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
/* Check the first side of the station */
|
||
int32_t direction = stationElement->GetDirectionWithOffset(1);
|
||
found = CheckForAdjacentStation(stationStart, direction);
|
||
if (found)
|
||
break;
|
||
/* Check the other side of the station */
|
||
direction = DirectionReverse(direction);
|
||
found = CheckForAdjacentStation(stationStart, direction);
|
||
if (found)
|
||
break;
|
||
}
|
||
}
|
||
return found;
|
||
}
|
||
|
||
bool RideHasStationShelter(const Ride& ride)
|
||
{
|
||
const auto* stationObj = ride.GetStationObject();
|
||
return stationObj != nullptr && (stationObj->Flags & STATION_OBJECT_FLAGS::HAS_SHELTER);
|
||
}
|
||
|
||
bool RideHasRatings(const Ride& ride)
|
||
{
|
||
return ride.excitement != kRideRatingUndefined;
|
||
}
|
||
|
||
int32_t GetBoosterSpeed(ride_type_t rideType, int32_t rawSpeed)
|
||
{
|
||
int8_t shiftFactor = GetRideTypeDescriptor(rideType).OperatingSettings.BoosterSpeedFactor;
|
||
if (shiftFactor == 0)
|
||
{
|
||
return rawSpeed;
|
||
}
|
||
if (shiftFactor > 0)
|
||
{
|
||
return (rawSpeed << shiftFactor);
|
||
}
|
||
|
||
// Workaround for an issue with older compilers (GCC 6, Clang 4) which would fail the build
|
||
int8_t shiftFactorAbs = std::abs(shiftFactor);
|
||
return (rawSpeed >> shiftFactorAbs);
|
||
}
|
||
|
||
void FixInvalidVehicleSpriteSizes()
|
||
{
|
||
for (const auto& ride : GetRideManager())
|
||
{
|
||
for (auto entityIndex : ride.vehicles)
|
||
{
|
||
for (Vehicle* vehicle = TryGetEntity<Vehicle>(entityIndex); vehicle != nullptr;
|
||
vehicle = TryGetEntity<Vehicle>(vehicle->next_vehicle_on_train))
|
||
{
|
||
auto carEntry = vehicle->Entry();
|
||
if (carEntry == nullptr)
|
||
{
|
||
break;
|
||
}
|
||
|
||
if (vehicle->SpriteData.Width == 0)
|
||
{
|
||
vehicle->SpriteData.Width = carEntry->sprite_width;
|
||
}
|
||
if (vehicle->SpriteData.HeightMin == 0)
|
||
{
|
||
vehicle->SpriteData.HeightMin = carEntry->sprite_height_negative;
|
||
}
|
||
if (vehicle->SpriteData.HeightMax == 0)
|
||
{
|
||
vehicle->SpriteData.HeightMax = carEntry->sprite_height_positive;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
bool RideEntryHasCategory(const RideObjectEntry& rideEntry, uint8_t category)
|
||
{
|
||
auto rideType = rideEntry.GetFirstNonNullRideType();
|
||
return GetRideTypeDescriptor(rideType).Category == category;
|
||
}
|
||
|
||
int32_t RideGetEntryIndex(int32_t rideType, int32_t rideSubType)
|
||
{
|
||
int32_t subType = rideSubType;
|
||
|
||
if (subType == OBJECT_ENTRY_INDEX_NULL)
|
||
{
|
||
auto& objManager = GetContext()->GetObjectManager();
|
||
auto& rideEntries = objManager.GetAllRideEntries(rideType);
|
||
if (rideEntries.size() > 0)
|
||
{
|
||
subType = rideEntries[0];
|
||
for (auto rideEntryIndex : rideEntries)
|
||
{
|
||
const auto* rideEntry = GetRideEntryByIndex(rideEntryIndex);
|
||
if (rideEntry == nullptr)
|
||
{
|
||
return OBJECT_ENTRY_INDEX_NULL;
|
||
}
|
||
|
||
// Can happen in select-by-track-type mode
|
||
if (!RideEntryIsInvented(rideEntryIndex) && !GetGameState().Cheats.IgnoreResearchStatus)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
|
||
{
|
||
subType = rideEntryIndex;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return subType;
|
||
}
|
||
|
||
const StationObject* Ride::GetStationObject() const
|
||
{
|
||
auto& objManager = GetContext()->GetObjectManager();
|
||
return static_cast<StationObject*>(objManager.GetLoadedObject(ObjectType::Station, entrance_style));
|
||
}
|
||
|
||
const MusicObject* Ride::GetMusicObject() const
|
||
{
|
||
auto& objManager = GetContext()->GetObjectManager();
|
||
return static_cast<MusicObject*>(objManager.GetLoadedObject(ObjectType::Music, music));
|
||
}
|
||
|
||
// Normally, a station has at most one entrance and one exit, which are at the same height
|
||
// as the station. But in hacked parks, neither can be taken for granted. This code ensures
|
||
// that the ride.entrances and ride.exits arrays will point to one of them. There is
|
||
// an ever-so-slight chance two entrances/exits for the same station reside on the same tile.
|
||
// In cases like this, the one at station height will be considered the "true" one.
|
||
// If none exists at that height, newer and higher placed ones take precedence.
|
||
void DetermineRideEntranceAndExitLocations()
|
||
{
|
||
LOG_VERBOSE("Inspecting ride entrance / exit locations");
|
||
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
for (auto& station : ride.GetStations())
|
||
{
|
||
auto stationIndex = ride.GetStationIndex(&station);
|
||
TileCoordsXYZD entranceLoc = station.Entrance;
|
||
TileCoordsXYZD exitLoc = station.Exit;
|
||
bool fixEntrance = false;
|
||
bool fixExit = false;
|
||
|
||
// Skip if the station has no entrance
|
||
if (!entranceLoc.IsNull())
|
||
{
|
||
const EntranceElement* entranceElement = MapGetRideEntranceElementAt(entranceLoc.ToCoordsXYZD(), false);
|
||
|
||
if (entranceElement == nullptr || entranceElement->GetRideIndex() != ride.id
|
||
|| entranceElement->GetStationIndex() != stationIndex)
|
||
{
|
||
fixEntrance = true;
|
||
}
|
||
else
|
||
{
|
||
station.Entrance.direction = static_cast<uint8_t>(entranceElement->GetDirection());
|
||
}
|
||
}
|
||
|
||
if (!exitLoc.IsNull())
|
||
{
|
||
const EntranceElement* entranceElement = MapGetRideExitElementAt(exitLoc.ToCoordsXYZD(), false);
|
||
|
||
if (entranceElement == nullptr || entranceElement->GetRideIndex() != ride.id
|
||
|| entranceElement->GetStationIndex() != stationIndex)
|
||
{
|
||
fixExit = true;
|
||
}
|
||
else
|
||
{
|
||
station.Exit.direction = static_cast<uint8_t>(entranceElement->GetDirection());
|
||
}
|
||
}
|
||
|
||
if (!fixEntrance && !fixExit)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// At this point, we know we have a disconnected entrance or exit.
|
||
// Search the map to find it. Skip the outer ring of invisible tiles.
|
||
bool alreadyFoundEntrance = false;
|
||
bool alreadyFoundExit = false;
|
||
auto& gameState = GetGameState();
|
||
for (int32_t y = 1; y < gameState.MapSize.y - 1; y++)
|
||
{
|
||
for (int32_t x = 1; x < gameState.MapSize.x - 1; x++)
|
||
{
|
||
TileElement* tileElement = MapGetFirstElementAt(TileCoordsXY{ x, y });
|
||
|
||
if (tileElement != nullptr)
|
||
{
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TileElementType::Entrance)
|
||
{
|
||
continue;
|
||
}
|
||
const EntranceElement* entranceElement = tileElement->AsEntrance();
|
||
if (entranceElement->GetRideIndex() != ride.id)
|
||
{
|
||
continue;
|
||
}
|
||
if (entranceElement->GetStationIndex() != stationIndex)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// The expected height is where entrances and exit reside in non-hacked parks.
|
||
const uint8_t expectedHeight = station.Height;
|
||
|
||
if (fixEntrance && entranceElement->GetEntranceType() == ENTRANCE_TYPE_RIDE_ENTRANCE)
|
||
{
|
||
if (alreadyFoundEntrance)
|
||
{
|
||
if (station.Entrance.z == expectedHeight)
|
||
continue;
|
||
if (station.Entrance.z > entranceElement->BaseHeight)
|
||
continue;
|
||
}
|
||
|
||
// Found our entrance
|
||
station.Entrance = { x, y, entranceElement->BaseHeight,
|
||
static_cast<uint8_t>(entranceElement->GetDirection()) };
|
||
alreadyFoundEntrance = true;
|
||
|
||
LOG_VERBOSE(
|
||
"Fixed disconnected entrance of ride %d, station %d to x = %d, y = %d and z = %d.", ride.id,
|
||
stationIndex, x, y, entranceElement->BaseHeight);
|
||
}
|
||
else if (fixExit && entranceElement->GetEntranceType() == ENTRANCE_TYPE_RIDE_EXIT)
|
||
{
|
||
if (alreadyFoundExit)
|
||
{
|
||
if (station.Exit.z == expectedHeight)
|
||
continue;
|
||
if (station.Exit.z > entranceElement->BaseHeight)
|
||
continue;
|
||
}
|
||
|
||
// Found our exit
|
||
station.Exit = { x, y, entranceElement->BaseHeight,
|
||
static_cast<uint8_t>(entranceElement->GetDirection()) };
|
||
alreadyFoundExit = true;
|
||
|
||
LOG_VERBOSE(
|
||
"Fixed disconnected exit of ride %d, station %d to x = %d, y = %d and z = %d.", ride.id,
|
||
stationIndex, x, y, entranceElement->BaseHeight);
|
||
}
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
}
|
||
|
||
if (fixEntrance && !alreadyFoundEntrance)
|
||
{
|
||
station.Entrance.SetNull();
|
||
LOG_VERBOSE("Cleared disconnected entrance of ride %d, station %d.", ride.id, stationIndex);
|
||
}
|
||
if (fixExit && !alreadyFoundExit)
|
||
{
|
||
station.Exit.SetNull();
|
||
LOG_VERBOSE("Cleared disconnected exit of ride %d, station %d.", ride.id, stationIndex);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void RideClearLeftoverEntrances(const Ride& ride)
|
||
{
|
||
auto& gameState = GetGameState();
|
||
for (TileCoordsXY tilePos = {}; tilePos.x < gameState.MapSize.x; ++tilePos.x)
|
||
{
|
||
for (tilePos.y = 0; tilePos.y < gameState.MapSize.y; ++tilePos.y)
|
||
{
|
||
for (auto* entrance : TileElementsView<EntranceElement>(tilePos.ToCoordsXY()))
|
||
{
|
||
const bool isRideEntranceExit = entrance->GetEntranceType() == ENTRANCE_TYPE_RIDE_ENTRANCE
|
||
|| entrance->GetEntranceType() == ENTRANCE_TYPE_RIDE_EXIT;
|
||
if (!isRideEntranceExit)
|
||
continue;
|
||
if (entrance->GetRideIndex() != ride.id)
|
||
continue;
|
||
|
||
TileElementRemove(entrance->as<TileElement>());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
std::string Ride::GetName() const
|
||
{
|
||
Formatter ft;
|
||
FormatNameTo(ft);
|
||
return FormatStringIDLegacy(STR_STRINGID, reinterpret_cast<const void*>(ft.Data()));
|
||
}
|
||
|
||
void Ride::FormatNameTo(Formatter& ft) const
|
||
{
|
||
if (!custom_name.empty())
|
||
{
|
||
auto str = custom_name.c_str();
|
||
ft.Add<StringId>(STR_STRING);
|
||
ft.Add<const char*>(str);
|
||
}
|
||
else
|
||
{
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
auto rideTypeName = rtd.Naming.Name;
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
|
||
{
|
||
auto rideEntry = GetRideEntry();
|
||
if (rideEntry != nullptr)
|
||
{
|
||
rideTypeName = rideEntry->naming.Name;
|
||
}
|
||
}
|
||
ft.Add<StringId>(1).Add<StringId>(rideTypeName).Add<uint16_t>(default_name_number);
|
||
}
|
||
}
|
||
|
||
uint64_t Ride::GetAvailableModes() const
|
||
{
|
||
if (GetGameState().Cheats.ShowAllOperatingModes)
|
||
return AllRideModesAvailable;
|
||
|
||
return GetRideTypeDescriptor().RideModes;
|
||
}
|
||
|
||
const RideTypeDescriptor& Ride::GetRideTypeDescriptor() const
|
||
{
|
||
return ::GetRideTypeDescriptor(type);
|
||
}
|
||
|
||
uint8_t Ride::GetNumShelteredSections() const
|
||
{
|
||
return num_sheltered_sections & ShelteredSectionsBits::NumShelteredSectionsMask;
|
||
}
|
||
|
||
void Ride::IncreaseNumShelteredSections()
|
||
{
|
||
auto newNumShelteredSections = GetNumShelteredSections();
|
||
if (newNumShelteredSections != 0x1F)
|
||
newNumShelteredSections++;
|
||
num_sheltered_sections &= ~ShelteredSectionsBits::NumShelteredSectionsMask;
|
||
num_sheltered_sections |= newNumShelteredSections;
|
||
}
|
||
|
||
void Ride::UpdateRideTypeForAllPieces()
|
||
{
|
||
auto& gameState = GetGameState();
|
||
for (int32_t y = 0; y < gameState.MapSize.y; y++)
|
||
{
|
||
for (int32_t x = 0; x < gameState.MapSize.x; x++)
|
||
{
|
||
auto* tileElement = MapGetFirstElementAt(TileCoordsXY(x, y));
|
||
if (tileElement == nullptr)
|
||
continue;
|
||
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TileElementType::Track)
|
||
continue;
|
||
|
||
auto* trackElement = tileElement->AsTrack();
|
||
if (trackElement->GetRideIndex() != id)
|
||
continue;
|
||
|
||
trackElement->SetRideType(type);
|
||
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
}
|
||
|
||
bool Ride::HasLifecycleFlag(uint32_t flag) const
|
||
{
|
||
return (lifecycle_flags & flag) != 0;
|
||
}
|
||
|
||
void Ride::SetLifecycleFlag(uint32_t flag, bool on)
|
||
{
|
||
if (on)
|
||
lifecycle_flags |= flag;
|
||
else
|
||
lifecycle_flags &= ~flag;
|
||
}
|
||
|
||
bool Ride::HasRecolourableShopItems() const
|
||
{
|
||
const auto rideEntry = GetRideEntry();
|
||
if (rideEntry == nullptr)
|
||
return false;
|
||
|
||
for (size_t itemIndex = 0; itemIndex < std::size(rideEntry->shop_item); itemIndex++)
|
||
{
|
||
const ShopItem currentItem = rideEntry->shop_item[itemIndex];
|
||
if (currentItem != ShopItem::None && GetShopItemDescriptor(currentItem).IsRecolourable())
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool Ride::HasStation() const
|
||
{
|
||
return num_stations != 0;
|
||
}
|
||
|
||
std::vector<RideId> GetTracklessRides()
|
||
{
|
||
// Iterate map and build list of seen ride IDs
|
||
std::vector<bool> seen;
|
||
seen.resize(256);
|
||
TileElementIterator it;
|
||
TileElementIteratorBegin(&it);
|
||
while (TileElementIteratorNext(&it))
|
||
{
|
||
auto trackEl = it.element->AsTrack();
|
||
if (trackEl != nullptr && !trackEl->IsGhost())
|
||
{
|
||
auto rideId = trackEl->GetRideIndex().ToUnderlying();
|
||
if (rideId >= seen.size())
|
||
{
|
||
seen.resize(rideId + 1);
|
||
}
|
||
seen[rideId] = true;
|
||
}
|
||
}
|
||
|
||
// Get all rides that did not get seen during map iteration
|
||
const auto& rideManager = GetRideManager();
|
||
std::vector<RideId> result;
|
||
for (const auto& ride : rideManager)
|
||
{
|
||
const auto rideIndex = ride.id.ToUnderlying();
|
||
if (seen.size() <= rideIndex || !seen[rideIndex])
|
||
{
|
||
result.push_back(ride.id);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
ResultWithMessage Ride::ChangeStatusDoStationChecks(StationIndex& stationIndex)
|
||
{
|
||
auto stationIndexCheck = RideModeCheckStationPresent(*this);
|
||
stationIndex = stationIndexCheck.StationIndex;
|
||
if (stationIndex.IsNull())
|
||
return { false, stationIndexCheck.Message };
|
||
|
||
auto stationNumbersCheck = RideModeCheckValidStationNumbers(*this);
|
||
if (!stationNumbersCheck.Successful)
|
||
return { false, stationNumbersCheck.Message };
|
||
|
||
return { true };
|
||
}
|
||
|
||
ResultWithMessage Ride::ChangeStatusGetStartElement(StationIndex stationIndex, CoordsXYE& trackElement)
|
||
{
|
||
auto startLoc = GetStation(stationIndex).Start;
|
||
trackElement.x = startLoc.x;
|
||
trackElement.y = startLoc.y;
|
||
trackElement.element = reinterpret_cast<TileElement*>(GetOriginElement(stationIndex));
|
||
if (trackElement.element == nullptr)
|
||
{
|
||
// Maze is strange, station start is 0... investigation required
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_IS_MAZE))
|
||
return { false };
|
||
}
|
||
|
||
return { true };
|
||
}
|
||
|
||
ResultWithMessage Ride::ChangeStatusCheckCompleteCircuit(const CoordsXYE& trackElement)
|
||
{
|
||
CoordsXYE problematicTrackElement = {};
|
||
if (mode == RideMode::Race || mode == RideMode::ContinuousCircuit || IsBlockSectioned())
|
||
{
|
||
if (FindTrackGap(trackElement, &problematicTrackElement))
|
||
{
|
||
RideScrollToTrackError(problematicTrackElement);
|
||
return { false, STR_TRACK_IS_NOT_A_COMPLETE_CIRCUIT };
|
||
}
|
||
}
|
||
|
||
return { true };
|
||
}
|
||
|
||
ResultWithMessage Ride::ChangeStatusCheckTrackValidity(const CoordsXYE& trackElement)
|
||
{
|
||
CoordsXYE problematicTrackElement = {};
|
||
|
||
if (IsBlockSectioned())
|
||
{
|
||
auto blockBrakeCheck = RideCheckBlockBrakes(trackElement, &problematicTrackElement);
|
||
if (!blockBrakeCheck.Successful)
|
||
{
|
||
RideScrollToTrackError(problematicTrackElement);
|
||
return { false, blockBrakeCheck.Message };
|
||
}
|
||
}
|
||
|
||
if (subtype != OBJECT_ENTRY_INDEX_NULL && !GetGameState().Cheats.EnableAllDrawableTrackPieces)
|
||
{
|
||
const auto* rideEntry = GetRideEntryByIndex(subtype);
|
||
if (rideEntry->flags & RIDE_ENTRY_FLAG_NO_INVERSIONS)
|
||
{
|
||
if (RideCheckTrackContainsInversions(trackElement, &problematicTrackElement))
|
||
{
|
||
RideScrollToTrackError(problematicTrackElement);
|
||
return { false, STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN };
|
||
}
|
||
}
|
||
if (rideEntry->flags & RIDE_ENTRY_FLAG_NO_BANKED_TRACK)
|
||
{
|
||
if (RideCheckTrackContainsBanked(trackElement, &problematicTrackElement))
|
||
{
|
||
RideScrollToTrackError(problematicTrackElement);
|
||
return { false, STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN };
|
||
}
|
||
}
|
||
}
|
||
|
||
if (mode == RideMode::StationToStation)
|
||
{
|
||
if (!FindTrackGap(trackElement, &problematicTrackElement))
|
||
{
|
||
return { false, STR_RIDE_MUST_START_AND_END_WITH_STATIONS };
|
||
}
|
||
|
||
if (!RideCheckStationLength(trackElement, &problematicTrackElement))
|
||
{
|
||
RideScrollToTrackError(problematicTrackElement);
|
||
return { false, STR_STATION_NOT_LONG_ENOUGH };
|
||
}
|
||
|
||
if (!RideCheckStartAndEndIsStation(trackElement))
|
||
{
|
||
RideScrollToTrackError(problematicTrackElement);
|
||
return { false, STR_RIDE_MUST_START_AND_END_WITH_STATIONS };
|
||
}
|
||
}
|
||
|
||
return { true };
|
||
}
|
||
|
||
ResultWithMessage Ride::ChangeStatusCreateVehicles(bool isApplying, const CoordsXYE& trackElement)
|
||
{
|
||
if (isApplying)
|
||
RideSetStartFinishPoints(id, trackElement);
|
||
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_NO_VEHICLES) && !(lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK))
|
||
{
|
||
const auto createVehicleResult = CreateVehicles(trackElement, isApplying);
|
||
if (!createVehicleResult.Successful)
|
||
{
|
||
return { false, createVehicleResult.Message };
|
||
}
|
||
}
|
||
|
||
if (rtd.HasFlag(RIDE_TYPE_FLAG_ALLOW_CABLE_LIFT_HILL) && (lifecycle_flags & RIDE_LIFECYCLE_CABLE_LIFT_HILL_COMPONENT_USED)
|
||
&& !(lifecycle_flags & RIDE_LIFECYCLE_CABLE_LIFT))
|
||
{
|
||
const auto createCableLiftResult = RideCreateCableLift(id, isApplying);
|
||
if (!createCableLiftResult.Successful)
|
||
return { false, createCableLiftResult.Message };
|
||
}
|
||
|
||
return { true };
|
||
}
|