mirror of https://github.com/OpenRCT2/OpenRCT2.git
5825 lines
184 KiB
C++
5825 lines
184 KiB
C++
/*****************************************************************************
|
||
* Copyright (c) 2014-2020 OpenRCT2 developers
|
||
*
|
||
* For a complete list of all authors, please refer to contributors.md
|
||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||
*
|
||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||
*****************************************************************************/
|
||
|
||
#include "Ride.h"
|
||
|
||
#include "../Cheats.h"
|
||
#include "../Context.h"
|
||
#include "../Editor.h"
|
||
#include "../Game.h"
|
||
#include "../Input.h"
|
||
#include "../OpenRCT2.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/FixedVector.h"
|
||
#include "../core/Guard.hpp"
|
||
#include "../core/Numerics.hpp"
|
||
#include "../interface/Window.h"
|
||
#include "../localisation/Date.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/StationObject.h"
|
||
#include "../paint/VirtualFloor.h"
|
||
#include "../peep/Peep.h"
|
||
#include "../peep/Staff.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/Footpath.h"
|
||
#include "../world/Location.hpp"
|
||
#include "../world/Map.h"
|
||
#include "../world/MapAnimation.h"
|
||
#include "../world/Park.h"
|
||
#include "../world/Scenery.h"
|
||
#include "../world/Sprite.h"
|
||
#include "CableLift.h"
|
||
#include "RideAudio.h"
|
||
#include "RideData.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 const int32_t RideInspectionInterval[] = {
|
||
10, 20, 30, 45, 60, 120, 0, 0,
|
||
};
|
||
|
||
static std::vector<Ride> _rides;
|
||
|
||
// Static function declarations
|
||
Staff* find_closest_mechanic(const CoordsXY& entrancePosition, int32_t forInspection);
|
||
static void ride_breakdown_status_update(Ride* ride);
|
||
static void ride_breakdown_update(Ride* ride);
|
||
static void ride_call_closest_mechanic(Ride* ride);
|
||
static void ride_call_mechanic(Ride* ride, Peep* mechanic, int32_t forInspection);
|
||
static void ride_entrance_exit_connected(Ride* ride);
|
||
static int32_t ride_get_new_breakdown_problem(Ride* ride);
|
||
static void ride_inspection_update(Ride* ride);
|
||
static void ride_mechanic_status_update(Ride* ride, int32_t mechanicStatus);
|
||
static void ride_music_update(Ride* ride);
|
||
static void ride_shop_connected(Ride* ride);
|
||
|
||
RideManager GetRideManager()
|
||
{
|
||
return {};
|
||
}
|
||
|
||
size_t RideManager::size() const
|
||
{
|
||
size_t count = 0;
|
||
for (size_t i = 0; i < _rides.size(); i++)
|
||
{
|
||
if (_rides[i].type != RIDE_TYPE_NULL)
|
||
{
|
||
count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
RideManager::Iterator RideManager::begin()
|
||
{
|
||
return RideManager::Iterator(*this, 0, _rides.size());
|
||
}
|
||
|
||
RideManager::Iterator RideManager::end()
|
||
{
|
||
return RideManager::Iterator(*this, _rides.size(), _rides.size());
|
||
}
|
||
|
||
ride_id_t GetNextFreeRideId()
|
||
{
|
||
size_t result = _rides.size();
|
||
for (size_t i = 0; i < _rides.size(); i++)
|
||
{
|
||
if (_rides[i].type == RIDE_TYPE_NULL)
|
||
{
|
||
result = i;
|
||
break;
|
||
}
|
||
}
|
||
if (result >= MAX_RIDES)
|
||
{
|
||
return RIDE_ID_NULL;
|
||
}
|
||
return static_cast<ride_id_t>(result);
|
||
}
|
||
|
||
Ride* GetOrAllocateRide(ride_id_t index)
|
||
{
|
||
const auto idx = static_cast<size_t>(index);
|
||
if (_rides.size() <= idx)
|
||
{
|
||
_rides.resize(idx + 1);
|
||
}
|
||
|
||
auto result = &_rides[idx];
|
||
result->id = index;
|
||
return result;
|
||
}
|
||
|
||
Ride* get_ride(ride_id_t index)
|
||
{
|
||
const auto idx = static_cast<size_t>(index);
|
||
if (idx < _rides.size())
|
||
{
|
||
auto& ride = _rides[idx];
|
||
if (ride.type != RIDE_TYPE_NULL)
|
||
{
|
||
assert(ride.id == index);
|
||
return &ride;
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
rct_ride_entry* get_ride_entry(ObjectEntryIndex index)
|
||
{
|
||
rct_ride_entry* result = nullptr;
|
||
auto& objMgr = OpenRCT2::GetContext()->GetObjectManager();
|
||
|
||
auto obj = objMgr.GetLoadedObject(ObjectType::Ride, index);
|
||
if (obj != nullptr)
|
||
{
|
||
result = static_cast<rct_ride_entry*>(obj->GetLegacyData());
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
std::string_view get_ride_entry_name(ObjectEntryIndex index)
|
||
{
|
||
if (index >= object_entry_group_counts[EnumValue(ObjectType::Ride)])
|
||
{
|
||
log_error("invalid index %d for ride type", index);
|
||
return {};
|
||
}
|
||
|
||
auto objectEntry = object_entry_get_object(ObjectType::Ride, index);
|
||
if (objectEntry != nullptr)
|
||
{
|
||
return objectEntry->GetLegacyIdentifier();
|
||
}
|
||
return {};
|
||
}
|
||
|
||
rct_ride_entry* Ride::GetRideEntry() const
|
||
{
|
||
return get_ride_entry(subtype);
|
||
}
|
||
|
||
int32_t ride_get_count()
|
||
{
|
||
return static_cast<int32_t>(GetRideManager().size());
|
||
}
|
||
|
||
size_t Ride::GetNumPrices() const
|
||
{
|
||
size_t result = 0;
|
||
if (type == RIDE_TYPE_CASH_MACHINE || type == RIDE_TYPE_FIRST_AID)
|
||
{
|
||
result = 0;
|
||
}
|
||
else if (type == RIDE_TYPE_TOILETS)
|
||
{
|
||
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 gDateMonthsElapsed - build_date;
|
||
}
|
||
|
||
int32_t Ride::GetTotalQueueLength() const
|
||
{
|
||
int32_t i, queueLength = 0;
|
||
for (i = 0; i < MAX_STATIONS; i++)
|
||
if (!ride_get_entrance_location(this, i).IsNull())
|
||
queueLength += stations[i].QueueLength;
|
||
return queueLength;
|
||
}
|
||
|
||
int32_t Ride::GetMaxQueueTime() const
|
||
{
|
||
uint8_t i, queueTime = 0;
|
||
for (i = 0; i < MAX_STATIONS; i++)
|
||
if (!ride_get_entrance_location(this, i).IsNull())
|
||
queueTime = std::max(queueTime, stations[i].QueueTime);
|
||
return static_cast<int32_t>(queueTime);
|
||
}
|
||
|
||
Guest* Ride::GetQueueHeadGuest(StationIndex stationIndex) const
|
||
{
|
||
Guest* peep;
|
||
Guest* result = nullptr;
|
||
uint16_t spriteIndex = stations[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;
|
||
uint16_t spriteIndex = stations[stationIndex].LastPeepInQueue;
|
||
while ((peep = TryGetEntity<Guest>(spriteIndex)) != nullptr)
|
||
{
|
||
spriteIndex = peep->GuestNextInQueue;
|
||
count++;
|
||
}
|
||
stations[stationIndex].QueueLength = count;
|
||
}
|
||
|
||
void Ride::QueueInsertGuestAtFront(StationIndex stationIndex, Guest* peep)
|
||
{
|
||
assert(stationIndex < MAX_STATIONS);
|
||
assert(peep != nullptr);
|
||
|
||
peep->GuestNextInQueue = SPRITE_INDEX_NULL;
|
||
auto* queueHeadGuest = GetQueueHeadGuest(peep->CurrentRideStation);
|
||
if (queueHeadGuest == nullptr)
|
||
{
|
||
stations[peep->CurrentRideStation].LastPeepInQueue = peep->sprite_index;
|
||
}
|
||
else
|
||
{
|
||
queueHeadGuest->GuestNextInQueue = peep->sprite_index;
|
||
}
|
||
UpdateQueueLength(peep->CurrentRideStation);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC916
|
||
*/
|
||
void ride_update_favourited_stat()
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
ride.guests_favourite = 0;
|
||
|
||
for (auto peep : EntityList<Guest>())
|
||
{
|
||
if (peep->FavouriteRide != RIDE_ID_NULL)
|
||
{
|
||
auto ride = get_ride(peep->FavouriteRide);
|
||
if (ride != nullptr)
|
||
{
|
||
ride->guests_favourite++;
|
||
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
||
}
|
||
}
|
||
}
|
||
|
||
window_invalidate_by_class(WC_RIDE_LIST);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006AC3AB
|
||
*/
|
||
money64 Ride::CalculateIncomePerHour() const
|
||
{
|
||
// Get entry by ride to provide better reporting
|
||
rct_ride_entry* entry = GetRideEntry();
|
||
if (entry == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
auto customersPerHour = ride_customers_per_hour(this);
|
||
money64 priceMinusCost = ride_get_price(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 money16 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 ride_try_get_origin_element(const Ride* ride, CoordsXYE* output)
|
||
{
|
||
TileElement* resultTileElement = nullptr;
|
||
|
||
tile_element_iterator it;
|
||
tile_element_iterator_begin(&it);
|
||
do
|
||
{
|
||
if (it.element->GetType() != TILE_ELEMENT_TYPE_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
|
||
&& (ted.SequenceProperties[0] & 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 (tile_element_iterator_next(&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 track_block_get_next_from_zero(
|
||
const CoordsXYZ& startPos, 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 = map_get_first_element_at(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 track_block_get_next(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 = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
const auto& ted = GetTrackElementDescriptor(inputElement->GetTrackType());
|
||
const auto* trackBlock = ted.Block;
|
||
if (trackBlock == nullptr)
|
||
return false;
|
||
|
||
trackBlock += inputElement->GetSequenceIndex();
|
||
|
||
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(direction_reverse(rotation));
|
||
|
||
OriginZ -= trackBlock->z;
|
||
OriginZ += trackCoordinate.z_end;
|
||
|
||
uint8_t directionStart = ((trackCoordinate.rotation_end + rotation) & TILE_ELEMENT_DIRECTION_MASK)
|
||
| (trackCoordinate.rotation_end & TRACK_BLOCK_2);
|
||
|
||
return track_block_get_next_from_zero({ 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 track_block_get_previous_from_zero(
|
||
const CoordsXYZ& startPos, Ride* ride, uint8_t direction, track_begin_end* outTrackBeginEnd)
|
||
{
|
||
uint8_t directionStart = direction;
|
||
direction = direction_reverse(direction);
|
||
auto trackPos = startPos;
|
||
|
||
if (!(direction & TRACK_BLOCK_2))
|
||
{
|
||
trackPos += CoordsDirectionDelta[direction];
|
||
}
|
||
|
||
TileElement* tileElement = map_get_first_element_at(trackPos);
|
||
if (tileElement == nullptr)
|
||
{
|
||
outTrackBeginEnd->end_x = trackPos.x;
|
||
outTrackBeginEnd->end_y = trackPos.y;
|
||
outTrackBeginEnd->begin_element = nullptr;
|
||
outTrackBeginEnd->begin_direction = direction_reverse(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* nextTrackBlock = ted->Block;
|
||
if (nextTrackBlock == nullptr)
|
||
continue;
|
||
const auto& nextTrackCoordinate = ted->Coordinates;
|
||
|
||
nextTrackBlock += trackElement->GetSequenceIndex();
|
||
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(direction_reverse(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 = direction_reverse(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 = direction_reverse(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 track_block_get_previous(const CoordsXYE& trackPos, track_begin_end* outTrackBeginEnd)
|
||
{
|
||
if (trackPos.element == nullptr)
|
||
return false;
|
||
|
||
auto trackElement = trackPos.element->AsTrack();
|
||
const auto& ted = GetTrackElementDescriptor(trackElement->GetTrackType());
|
||
if (trackElement == nullptr)
|
||
return false;
|
||
|
||
auto rideIndex = trackElement->GetRideIndex();
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
const auto* trackBlock = ted.Block;
|
||
if (trackBlock == nullptr)
|
||
return false;
|
||
|
||
trackBlock += trackElement->GetSequenceIndex();
|
||
|
||
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(direction_reverse(rotation));
|
||
|
||
z -= trackBlock->z;
|
||
z += trackCoordinate.z_begin;
|
||
|
||
rotation = ((trackCoordinate.rotation_begin + rotation) & TILE_ELEMENT_DIRECTION_MASK)
|
||
| (trackCoordinate.rotation_begin & TRACK_BLOCK_2);
|
||
|
||
return track_block_get_previous_from_zero({ 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
|
||
*/
|
||
int32_t ride_find_track_gap(const Ride* ride, CoordsXYE* input, CoordsXYE* output)
|
||
{
|
||
if (ride == nullptr || input == nullptr || input->element == nullptr
|
||
|| input->element->GetType() != TILE_ELEMENT_TYPE_TRACK)
|
||
return 0;
|
||
|
||
if (ride->type == RIDE_TYPE_MAZE)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
rct_window* w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && _currentRideIndex == ride->id)
|
||
{
|
||
ride_construction_invalidate_current_track();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
track_circuit_iterator it = {};
|
||
track_circuit_iterator_begin(&it, *input);
|
||
track_circuit_iterator slowIt = it;
|
||
while (track_circuit_iterator_next(&it))
|
||
{
|
||
if (!track_is_connected_by_shape(it.last.element, it.current.element))
|
||
{
|
||
*output = it.current;
|
||
return 1;
|
||
}
|
||
//#2081: prevent an infinite loop
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
track_circuit_iterator_next(&slowIt);
|
||
if (track_circuit_iterators_match(&it, &slowIt))
|
||
{
|
||
*output = it.current;
|
||
return 1;
|
||
}
|
||
}
|
||
}
|
||
if (!it.looped)
|
||
{
|
||
*output = it.last;
|
||
return 1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
void Ride::FormatStatusTo(Formatter& ft) const
|
||
{
|
||
if (lifecycle_flags & RIDE_LIFECYCLE_CRASHED)
|
||
{
|
||
ft.Add<rct_string_id>(STR_CRASHED);
|
||
}
|
||
else if (lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
|
||
{
|
||
ft.Add<rct_string_id>(STR_BROKEN_DOWN);
|
||
}
|
||
else if (status == RideStatus::Closed)
|
||
{
|
||
if (!GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP))
|
||
{
|
||
if (num_riders != 0)
|
||
{
|
||
ft.Add<rct_string_id>(num_riders == 1 ? STR_CLOSED_WITH_PERSON : STR_CLOSED_WITH_PEOPLE);
|
||
ft.Add<uint16_t>(num_riders);
|
||
}
|
||
else
|
||
{
|
||
ft.Add<rct_string_id>(STR_CLOSED);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ft.Add<rct_string_id>(STR_CLOSED);
|
||
}
|
||
}
|
||
else if (status == RideStatus::Simulating)
|
||
{
|
||
ft.Add<rct_string_id>(STR_SIMULATING);
|
||
}
|
||
else if (status == RideStatus::Testing)
|
||
{
|
||
ft.Add<rct_string_id>(STR_TEST_RUN);
|
||
}
|
||
else if (
|
||
mode == RideMode::Race && !(lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING)
|
||
&& race_winner != SPRITE_INDEX_NULL)
|
||
{
|
||
auto peep = GetEntity<Guest>(race_winner);
|
||
if (peep != nullptr)
|
||
{
|
||
ft.Add<rct_string_id>(STR_RACE_WON_BY);
|
||
peep->FormatNameTo(ft);
|
||
}
|
||
else
|
||
{
|
||
ft.Add<rct_string_id>(STR_RACE_WON_BY);
|
||
ft.Add<rct_string_id>(STR_NONE);
|
||
}
|
||
}
|
||
else if (!GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP))
|
||
{
|
||
ft.Add<rct_string_id>(num_riders == 1 ? STR_PERSON_ON_RIDE : STR_PEOPLE_ON_RIDE);
|
||
ft.Add<uint16_t>(num_riders);
|
||
}
|
||
else
|
||
{
|
||
ft.Add<rct_string_id>(STR_OPEN);
|
||
}
|
||
}
|
||
|
||
int32_t ride_get_total_length(const Ride* ride)
|
||
{
|
||
int32_t i, totalLength = 0;
|
||
for (i = 0; i < ride->num_stations; i++)
|
||
totalLength += ride->stations[i].SegmentLength;
|
||
return totalLength;
|
||
}
|
||
|
||
int32_t ride_get_total_time(Ride* ride)
|
||
{
|
||
int32_t i, totalTime = 0;
|
||
for (i = 0; i < ride->num_stations; i++)
|
||
totalTime += ride->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 (num_vehicles > 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 ride_init_all()
|
||
{
|
||
_rides.clear();
|
||
_rides.shrink_to_fit();
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7A38
|
||
*/
|
||
void reset_all_ride_build_dates()
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
ride.build_date -= gDateMonthsElapsed;
|
||
}
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Construction
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Update functions
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006ABE4C
|
||
*/
|
||
void Ride::UpdateAll()
|
||
{
|
||
// Remove all rides if scenario editor
|
||
if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR)
|
||
{
|
||
switch (gEditorStep)
|
||
{
|
||
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;
|
||
}
|
||
|
||
window_update_viewport_ride_music();
|
||
|
||
// Update rides
|
||
for (auto& ride : GetRideManager())
|
||
ride.Update();
|
||
|
||
OpenRCT2::RideAudio::UpdateMusicChannels();
|
||
}
|
||
|
||
std::unique_ptr<TrackDesign> Ride::SaveToTrackDesign() const
|
||
{
|
||
if (!(lifecycle_flags & RIDE_LIFECYCLE_TESTED))
|
||
{
|
||
context_show_error(STR_CANT_SAVE_TRACK_DESIGN, STR_NONE, {});
|
||
return nullptr;
|
||
}
|
||
|
||
if (!ride_has_ratings(this))
|
||
{
|
||
context_show_error(STR_CANT_SAVE_TRACK_DESIGN, STR_NONE, {});
|
||
return nullptr;
|
||
}
|
||
|
||
auto td = std::make_unique<TrackDesign>();
|
||
auto errMessage = td->CreateTrackDesign(*this);
|
||
if (errMessage != STR_NONE)
|
||
{
|
||
context_show_error(STR_CANT_SAVE_TRACK_DESIGN, errMessage, {});
|
||
return nullptr;
|
||
}
|
||
|
||
return td;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006ABE73
|
||
*/
|
||
void Ride::Update()
|
||
{
|
||
if (vehicle_change_timeout != 0)
|
||
vehicle_change_timeout--;
|
||
|
||
ride_music_update(this);
|
||
|
||
// Update stations
|
||
if (type != RIDE_TYPE_MAZE)
|
||
for (int32_t i = 0; i < MAX_STATIONS; i++)
|
||
ride_update_station(this, 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 = CUSTOMER_HISTORY_SIZE - 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 != MONEY16_UNDEFINED)
|
||
profit = (income_per_hour - (static_cast<money32>(upkeep_cost * 16)));
|
||
}
|
||
|
||
// Ride specific updates
|
||
if (type == RIDE_TYPE_CHAIRLIFT)
|
||
UpdateChairlift();
|
||
else if (type == RIDE_TYPE_SPIRAL_SLIDE)
|
||
UpdateSpiralSlide();
|
||
|
||
ride_breakdown_update(this);
|
||
|
||
// Various things include news messages
|
||
if (lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_DUE_INSPECTION))
|
||
{
|
||
// Breakdown updates are distributed, only one ride can update the breakdown status per tick.
|
||
const auto updatingRideId = (gCurrentTicks / 2) % MAX_RIDES;
|
||
if (static_cast<ride_id_t>(updatingRideId) == id)
|
||
ride_breakdown_status_update(this);
|
||
}
|
||
|
||
ride_inspection_update(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 Ride::UpdateChairlift()
|
||
{
|
||
if (!(lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK))
|
||
return;
|
||
if ((lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_CRASHED))
|
||
&& breakdown_reason_pending == 0)
|
||
return;
|
||
|
||
uint16_t old_chairlift_bullwheel_rotation = chairlift_bullwheel_rotation >> 14;
|
||
chairlift_bullwheel_rotation += speed * 2048;
|
||
if (old_chairlift_bullwheel_rotation == speed / 8)
|
||
return;
|
||
|
||
auto bullwheelLoc = ChairliftBullwheelLocation[0].ToCoordsXYZ();
|
||
map_invalidate_tile_zoom1({ bullwheelLoc, bullwheelLoc.z, bullwheelLoc.z + (4 * COORDS_Z_STEP) });
|
||
|
||
bullwheelLoc = ChairliftBullwheelLocation[1].ToCoordsXYZ();
|
||
map_invalidate_tile_zoom1({ 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_update_satisfaction(Ride* ride, uint8_t happiness)
|
||
{
|
||
ride->satisfaction_next += happiness;
|
||
ride->satisfaction_time_out++;
|
||
if (ride->satisfaction_time_out >= 20)
|
||
{
|
||
ride->satisfaction = ride->satisfaction_next >> 2;
|
||
ride->satisfaction_next = 0;
|
||
ride->satisfaction_time_out = 0;
|
||
ride->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_update_popularity(Ride* ride, uint8_t pop_amount)
|
||
{
|
||
ride->popularity_next += pop_amount;
|
||
ride->popularity_time_out++;
|
||
if (ride->popularity_time_out < 25)
|
||
return;
|
||
|
||
ride->popularity = ride->popularity_next;
|
||
ride->popularity_next = 0;
|
||
ride->popularity_time_out = 0;
|
||
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
||
}
|
||
|
||
/** rct2: 0x0098DDB8, 0x0098DDBA */
|
||
static constexpr const 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 Ride::UpdateSpiralSlide()
|
||
{
|
||
if (gCurrentTicks & 3)
|
||
return;
|
||
if (slide_in_use == 0)
|
||
return;
|
||
|
||
spiral_slide_progress++;
|
||
if (spiral_slide_progress >= 48)
|
||
{
|
||
slide_in_use--;
|
||
|
||
auto* peep = GetEntity<Guest>(slide_peep);
|
||
if (peep != nullptr)
|
||
{
|
||
auto destination = peep->GetDestination();
|
||
destination.x++;
|
||
peep->SetDestination(destination);
|
||
}
|
||
}
|
||
|
||
const uint8_t current_rotation = get_current_rotation();
|
||
// Invalidate something related to station start
|
||
for (int32_t i = 0; i < MAX_STATIONS; i++)
|
||
{
|
||
if (stations[i].Start.IsNull())
|
||
continue;
|
||
|
||
auto startLoc = stations[i].Start;
|
||
|
||
TileElement* tileElement = ride_get_station_start_track_element(this, i);
|
||
if (tileElement == nullptr)
|
||
continue;
|
||
|
||
int32_t rotation = tileElement->GetDirection();
|
||
startLoc += ride_spiral_slide_main_tile_offset[rotation][current_rotation];
|
||
|
||
map_invalidate_tile_zoom0({ 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 ride_inspection_update(Ride* ride)
|
||
{
|
||
if (gCurrentTicks & 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 = ride_get_first_valid_station_exit(ride);
|
||
ride->inspection_station = (stationIndex != STATION_INDEX_NULL) ? stationIndex : 0;
|
||
}
|
||
|
||
static int32_t get_age_penalty(Ride* ride)
|
||
{
|
||
auto years = date_get_year(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 ride_breakdown_update(Ride* ride)
|
||
{
|
||
if (gCurrentTicks & 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 (!(gCurrentTicks & 8191))
|
||
{
|
||
int32_t totalDowntime = 0;
|
||
|
||
for (int32_t i = 0; i < DOWNTIME_HISTORY_SIZE; i++)
|
||
{
|
||
totalDowntime += ride->downtime_history[i];
|
||
}
|
||
|
||
ride->downtime = std::min(totalDowntime / 2, 100);
|
||
|
||
for (int32_t i = DOWNTIME_HISTORY_SIZE - 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 = RIDE_INITIAL_RELIABILITY;
|
||
return;
|
||
}
|
||
|
||
// Calculate breakdown probability?
|
||
int32_t unreliabilityAccumulator = ride->unreliability_factor + get_age_penalty(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<int32_t>(scenario_rand() & 0x2FFFFF) <= 1 + RIDE_INITIAL_RELIABILITY - ride->reliability)
|
||
&& !gCheatsDisableAllBreakdowns)
|
||
{
|
||
int32_t breakdownReason = ride_get_new_breakdown_problem(ride);
|
||
if (breakdownReason != -1)
|
||
ride_prepare_breakdown(ride, breakdownReason);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7294
|
||
*/
|
||
static int32_t ride_get_new_breakdown_problem(Ride* ride)
|
||
{
|
||
int32_t availableBreakdownProblems, totalProbability, randomProbability, problemBits, breakdownProblem;
|
||
|
||
// Brake failure is more likely when it's raining
|
||
_breakdownProblemProbabilities[BREAKDOWN_BRAKES_FAILURE] = climate_is_raining() ? 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 = bitscanforward(problemBits);
|
||
problemBits &= ~(1 << breakdownProblem);
|
||
totalProbability += _breakdownProblemProbabilities[breakdownProblem];
|
||
}
|
||
if (totalProbability == 0)
|
||
return -1;
|
||
|
||
// Choose a random number within this range
|
||
randomProbability = scenario_rand() % totalProbability;
|
||
|
||
// Find which problem range the random number lies
|
||
problemBits = availableBreakdownProblems;
|
||
do
|
||
{
|
||
breakdownProblem = bitscanforward(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->num_vehicles != 1)
|
||
return -1;
|
||
|
||
// If brakes failure is disabled, also take it out of the equation (see above comment why)
|
||
if (gCheatsDisableBrakesFailure)
|
||
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;
|
||
}
|
||
|
||
rct_ride_entry* entry = GetRideEntry();
|
||
return entry != nullptr && !(entry->flags & RIDE_ENTRY_FLAG_CANNOT_BREAK_DOWN);
|
||
}
|
||
|
||
static void choose_random_train_to_breakdown_safe(Ride* ride)
|
||
{
|
||
// Prevent integer division by zero in case of hacked ride.
|
||
if (ride->num_vehicles == 0)
|
||
return;
|
||
|
||
ride->broken_vehicle = scenario_rand() % ride->num_vehicles;
|
||
|
||
// 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] == SPRITE_INDEX_NULL && ride->broken_vehicle != 0)
|
||
{
|
||
--ride->broken_vehicle;
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7348
|
||
*/
|
||
void ride_prepare_breakdown(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 = 0; // ensure set to something.
|
||
|
||
switch (breakdownReason)
|
||
{
|
||
case BREAKDOWN_SAFETY_CUT_OUT:
|
||
case BREAKDOWN_CONTROL_FAILURE:
|
||
// Inspect first station with an exit
|
||
i = ride_get_first_valid_station_exit(ride);
|
||
if (i != STATION_INDEX_NULL)
|
||
{
|
||
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
|
||
choose_random_train_to_breakdown_safe(ride);
|
||
if (ride->num_cars_per_train != 0)
|
||
{
|
||
ride->broken_car = scenario_rand() % 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->SetUpdateFlag(VEHICLE_UPDATE_FLAG_BROKEN_CAR);
|
||
}
|
||
}
|
||
break;
|
||
case BREAKDOWN_VEHICLE_MALFUNCTION:
|
||
// Choose a random train
|
||
choose_random_train_to_breakdown_safe(ride);
|
||
ride->broken_car = 0;
|
||
|
||
// Set flag on broken train, first car
|
||
vehicle = GetEntity<Vehicle>(ride->vehicles[ride->broken_vehicle]);
|
||
if (vehicle != nullptr)
|
||
{
|
||
vehicle->SetUpdateFlag(VEHICLE_UPDATE_FLAG_BROKEN_TRAIN);
|
||
}
|
||
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 = ride_get_first_valid_station_exit(ride);
|
||
if (i != STATION_INDEX_NULL)
|
||
{
|
||
ride->inspection_station = i;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B74FA
|
||
*/
|
||
void ride_breakdown_add_news_item(Ride* ride)
|
||
{
|
||
if (gConfigNotifications.ride_broken_down)
|
||
{
|
||
Formatter ft;
|
||
ride->FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_RIDE_IS_BROKEN_DOWN, EnumValue(ride->id), ft);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B75C8
|
||
*/
|
||
static void ride_breakdown_status_update(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.ride_warnings)
|
||
{
|
||
Formatter ft;
|
||
ride->FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_RIDE_IS_STILL_NOT_FIXED, EnumValue(ride->id), ft);
|
||
}
|
||
}
|
||
}
|
||
|
||
ride_mechanic_status_update(ride, ride->mechanic_status);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B762F
|
||
*/
|
||
static void ride_mechanic_status_update(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;
|
||
ride_breakdown_add_news_item(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;
|
||
}
|
||
|
||
ride_call_closest_mechanic(ride);
|
||
break;
|
||
case RIDE_MECHANIC_STATUS_HEADING:
|
||
{
|
||
auto mechanic = ride_get_mechanic(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;
|
||
ride_mechanic_status_update(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)
|
||
ride_call_mechanic(ride, mechanic, false);
|
||
}
|
||
break;
|
||
}
|
||
case RIDE_MECHANIC_STATUS_FIXING:
|
||
{
|
||
auto mechanic = ride_get_mechanic(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;
|
||
ride_mechanic_status_update(ride, RIDE_MECHANIC_STATUS_CALLING);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B796C
|
||
*/
|
||
static void ride_call_mechanic(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->sprite_index;
|
||
mechanic->CurrentRide = ride->id;
|
||
mechanic->CurrentRideStation = ride->inspection_station;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B76AB
|
||
*/
|
||
static void ride_call_closest_mechanic(Ride* ride)
|
||
{
|
||
auto forInspection = (ride->lifecycle_flags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN)) == 0;
|
||
auto mechanic = ride_find_closest_mechanic(ride, forInspection);
|
||
if (mechanic != nullptr)
|
||
ride_call_mechanic(ride, mechanic, forInspection);
|
||
}
|
||
|
||
Staff* ride_find_closest_mechanic(Ride* ride, int32_t forInspection)
|
||
{
|
||
// Get either exit position or entrance position if there is no exit
|
||
auto stationIndex = ride->inspection_station;
|
||
TileCoordsXYZD location = ride_get_exit_location(ride, stationIndex);
|
||
if (location.IsNull())
|
||
{
|
||
location = ride_get_entrance_location(ride, stationIndex);
|
||
if (location.IsNull())
|
||
return nullptr;
|
||
}
|
||
|
||
// Get station start track element and position
|
||
auto mapLocation = location.ToCoordsXYZ();
|
||
TileElement* tileElement = ride_get_station_exit_element(mapLocation);
|
||
if (tileElement == nullptr)
|
||
return nullptr;
|
||
|
||
// Set x,y to centre of the station exit for the mechanic search.
|
||
auto centreMapLocation = mapLocation.ToTileCentre();
|
||
|
||
return find_closest_mechanic(centreMapLocation, forInspection);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B774B (forInspection = 0)
|
||
* rct2: 0x006B78C3 (forInspection = 1)
|
||
*/
|
||
Staff* find_closest_mechanic(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 (map_is_location_in_park(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* ride_get_mechanic(Ride* ride)
|
||
{
|
||
auto staff = GetEntity<Staff>(ride->mechanic);
|
||
if (staff != nullptr && staff->IsMechanic())
|
||
{
|
||
return staff;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
Staff* ride_get_assigned_mechanic(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 ride_get_mechanic(ride);
|
||
}
|
||
}
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Music functions
|
||
|
||
/**
|
||
*
|
||
* Calculates the sample rate for ride music.
|
||
*/
|
||
static int32_t RideMusicSampleRate(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 (!(gCurrentTicks & 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 = 255;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Circus music is a sound effect, rather than music. Needs separate processing.
|
||
*/
|
||
static 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 = 255;
|
||
return;
|
||
}
|
||
|
||
if (RideMusicBreakdownEffect(ride))
|
||
{
|
||
return;
|
||
}
|
||
|
||
CoordsXYZ rideCoords = ride->stations[0].GetStart().ToTileCentre();
|
||
|
||
const auto sampleRate = RideMusicSampleRate(ride);
|
||
|
||
OpenRCT2::RideAudio::UpdateMusicInstance(*ride, rideCoords, sampleRate);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006ABE85
|
||
*/
|
||
static void ride_music_update(Ride* ride)
|
||
{
|
||
if (ride->type == RIDE_TYPE_CIRCUS)
|
||
{
|
||
CircusMusicUpdate(ride);
|
||
return;
|
||
}
|
||
|
||
const auto& rtd = ride->GetRideTypeDescriptor();
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_MUSIC_ON_DEFAULT) && !rtd.HasFlag(RIDE_TYPE_FLAG_ALLOW_MUSIC))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (ride->status != RideStatus::Open || !(ride->lifecycle_flags & RIDE_LIFECYCLE_MUSIC))
|
||
{
|
||
ride->music_tune_id = 255;
|
||
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 == 255)
|
||
{
|
||
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>(util_rand() % numTracks);
|
||
ride->music_position = 0;
|
||
}
|
||
return;
|
||
}
|
||
|
||
CoordsXYZ rideCoords = ride->stations[0].GetStart().ToTileCentre();
|
||
|
||
int32_t sampleRate = RideMusicSampleRate(ride);
|
||
|
||
OpenRCT2::RideAudio::UpdateMusicInstance(*ride, rideCoords, sampleRate);
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Measurement functions
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B64F2
|
||
*/
|
||
static void ride_measurement_update(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)
|
||
if (vehicle->velocity == 0)
|
||
return;
|
||
|
||
if (measurement.current_item >= RideMeasurement::MAX_ITEMS)
|
||
return;
|
||
|
||
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 (gCurrentTicks & 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 (gCurrentTicks & 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 (gCurrentTicks & 1)
|
||
{
|
||
measurement.current_item++;
|
||
measurement.num_items = std::max(measurement.num_items, measurement.current_item);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B6456
|
||
*/
|
||
void ride_measurements_update()
|
||
{
|
||
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)
|
||
{
|
||
ride_measurement_update(ride, *measurement);
|
||
}
|
||
else
|
||
{
|
||
// For each vehicle
|
||
for (int32_t j = 0; j < ride.num_vehicles; j++)
|
||
{
|
||
uint16_t 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;
|
||
ride_measurement_update(ride, *measurement);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* If there are more than the threshold of allowed ride measurements, free the non-LRU one.
|
||
*/
|
||
static void ride_free_old_measurements()
|
||
{
|
||
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 > MAX_RIDE_MEASUREMENTS && lruRide != nullptr)
|
||
{
|
||
lruRide->measurement = {};
|
||
numRideMeasurements--;
|
||
}
|
||
} while (numRideMeasurements > MAX_RIDE_MEASUREMENTS);
|
||
}
|
||
|
||
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;
|
||
}
|
||
ride_free_old_measurements();
|
||
assert(measurement != nullptr);
|
||
}
|
||
|
||
measurement->last_use_tick = gCurrentTicks;
|
||
if (measurement->flags & 1)
|
||
{
|
||
return { measurement.get(), { STR_EMPTY, {} } };
|
||
}
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<rct_string_id>(GetRideComponentName(GetRideTypeDescriptor().NameConvention.vehicle).singular);
|
||
ft.Add<rct_string_id>(GetRideComponentName(GetRideTypeDescriptor().NameConvention.station).singular);
|
||
return { nullptr, { STR_DATA_LOGGING_WILL_START_WHEN_NEXT_LEAVES, ft } };
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Colour functions
|
||
|
||
TrackColour ride_get_track_colour(Ride* ride, int32_t colourScheme)
|
||
{
|
||
TrackColour result;
|
||
result.main = ride->track_colour[colourScheme].main;
|
||
result.additional = ride->track_colour[colourScheme].additional;
|
||
result.supports = ride->track_colour[colourScheme].supports;
|
||
return result;
|
||
}
|
||
|
||
vehicle_colour ride_get_vehicle_colour(Ride* ride, int32_t vehicleIndex)
|
||
{
|
||
vehicle_colour result;
|
||
|
||
// Prevent indexing array out of bounds
|
||
vehicleIndex = std::min<int32_t>(vehicleIndex, MAX_CARS_PER_TRAIN);
|
||
|
||
result.main = ride->vehicle_colours[vehicleIndex].Body;
|
||
result.additional_1 = ride->vehicle_colours[vehicleIndex].Trim;
|
||
result.additional_2 = ride->vehicle_colours[vehicleIndex].Ternary;
|
||
return result;
|
||
}
|
||
|
||
static bool ride_does_vehicle_colour_exist(ObjectEntryIndex subType, vehicle_colour* vehicleColour)
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.subtype != subType)
|
||
continue;
|
||
if (ride.vehicle_colours[0].Body != vehicleColour->main)
|
||
continue;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
int32_t ride_get_unused_preset_vehicle_colour(ObjectEntryIndex subType)
|
||
{
|
||
if (subType >= MAX_RIDE_OBJECTS)
|
||
{
|
||
return 0;
|
||
}
|
||
rct_ride_entry* rideEntry = get_ride_entry(subType);
|
||
if (rideEntry == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
vehicle_colour_preset_list* presetList = rideEntry->vehicle_preset_list;
|
||
if (presetList->count == 0)
|
||
return 0;
|
||
if (presetList->count == 255)
|
||
return 255;
|
||
|
||
for (int32_t attempt = 0; attempt < 200; attempt++)
|
||
{
|
||
uint8_t numColourConfigurations = presetList->count;
|
||
int32_t randomConfigIndex = util_rand() % numColourConfigurations;
|
||
vehicle_colour* preset = &presetList->list[randomConfigIndex];
|
||
|
||
if (ride_does_vehicle_colour_exist(subType, preset))
|
||
{
|
||
return randomConfigIndex;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DE52C
|
||
*/
|
||
void ride_set_vehicle_colours_to_random_preset(Ride* ride, uint8_t preset_index)
|
||
{
|
||
rct_ride_entry* rideEntry = get_ride_entry(ride->subtype);
|
||
vehicle_colour_preset_list* presetList = rideEntry->vehicle_preset_list;
|
||
|
||
if (presetList->count != 0 && presetList->count != 255)
|
||
{
|
||
assert(preset_index < presetList->count);
|
||
|
||
ride->colour_scheme_type = RIDE_COLOUR_SCHEME_ALL_SAME;
|
||
vehicle_colour* preset = &presetList->list[preset_index];
|
||
ride->vehicle_colours[0].Body = preset->main;
|
||
ride->vehicle_colours[0].Trim = preset->additional_1;
|
||
ride->vehicle_colours[0].Ternary = preset->additional_2;
|
||
}
|
||
else
|
||
{
|
||
ride->colour_scheme_type = RIDE_COLOUR_SCHEME_DIFFERENT_PER_TRAIN;
|
||
uint32_t count = std::min(presetList->count, static_cast<uint8_t>(32));
|
||
for (uint32_t i = 0; i < count; i++)
|
||
{
|
||
vehicle_colour* preset = &presetList->list[i];
|
||
ride->vehicle_colours[i].Body = preset->main;
|
||
ride->vehicle_colours[i].Trim = preset->additional_1;
|
||
ride->vehicle_colours[i].Ternary = preset->additional_2;
|
||
}
|
||
}
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Reachability
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7A5E
|
||
*/
|
||
void ride_check_all_reachable()
|
||
{
|
||
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))
|
||
ride_shop_connected(&ride);
|
||
else
|
||
ride_entrance_exit_connected(&ride);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7C59
|
||
* @return true if the coordinate is reachable or has no entrance, false otherwise
|
||
*/
|
||
static bool ride_entrance_exit_is_reachable(const TileCoordsXYZD& coordinates)
|
||
{
|
||
if (coordinates.IsNull())
|
||
return true;
|
||
|
||
TileCoordsXYZ loc{ coordinates.x, coordinates.y, coordinates.z };
|
||
loc -= TileDirectionDelta[coordinates.direction];
|
||
|
||
return map_coord_is_connected(loc, coordinates.direction);
|
||
}
|
||
|
||
static void ride_entrance_exit_connected(Ride* ride)
|
||
{
|
||
for (int32_t i = 0; i < MAX_STATIONS; ++i)
|
||
{
|
||
auto station_start = ride->stations[i].Start;
|
||
auto entrance = ride_get_entrance_location(ride, i);
|
||
auto exit = ride_get_exit_location(ride, i);
|
||
|
||
if (station_start.IsNull())
|
||
continue;
|
||
if (!entrance.IsNull() && !ride_entrance_exit_is_reachable(entrance))
|
||
{
|
||
// name of ride is parameter of the format string
|
||
Formatter ft;
|
||
ride->FormatNameTo(ft);
|
||
if (gConfigNotifications.ride_warnings)
|
||
{
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_ENTRANCE_NOT_CONNECTED, EnumValue(ride->id), ft);
|
||
}
|
||
ride->connected_message_throttle = 3;
|
||
}
|
||
|
||
if (!exit.IsNull() && !ride_entrance_exit_is_reachable(exit))
|
||
{
|
||
// name of ride is parameter of the format string
|
||
Formatter ft;
|
||
ride->FormatNameTo(ft);
|
||
if (gConfigNotifications.ride_warnings)
|
||
{
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_EXIT_NOT_CONNECTED, EnumValue(ride->id), ft);
|
||
}
|
||
ride->connected_message_throttle = 3;
|
||
}
|
||
}
|
||
}
|
||
|
||
static void ride_shop_connected(Ride* ride)
|
||
{
|
||
auto shopLoc = TileCoordsXY(ride->stations[0].Start);
|
||
if (shopLoc.IsNull())
|
||
return;
|
||
|
||
TrackElement* trackElement = nullptr;
|
||
TileElement* tileElement = map_get_first_element_at(shopLoc);
|
||
do
|
||
{
|
||
if (tileElement == nullptr)
|
||
break;
|
||
if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK && tileElement->AsTrack()->GetRideIndex() == ride->id)
|
||
{
|
||
trackElement = tileElement->AsTrack();
|
||
break;
|
||
}
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
|
||
if (trackElement == nullptr)
|
||
return;
|
||
|
||
auto track_type = trackElement->GetTrackType();
|
||
ride = get_ride(trackElement->GetRideIndex());
|
||
if (ride == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const auto& ted = GetTrackElementDescriptor(track_type);
|
||
uint8_t entrance_directions = ted.SequenceProperties[0] & 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 = direction_reverse(count);
|
||
|
||
int32_t y2 = shopLoc.y - TileDirectionDelta[face_direction].y;
|
||
int32_t x2 = shopLoc.x - TileDirectionDelta[face_direction].x;
|
||
|
||
if (map_coord_is_connected({ x2, y2, tileElement->base_height }, face_direction))
|
||
return;
|
||
}
|
||
|
||
// Name of ride is parameter of the format string
|
||
if (gConfigNotifications.ride_warnings)
|
||
{
|
||
Formatter ft;
|
||
ride->FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_ENTRANCE_NOT_CONNECTED, EnumValue(ride->id), ft);
|
||
}
|
||
|
||
ride->connected_message_throttle = 3;
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
#pragma region Interface
|
||
|
||
static void ride_track_set_map_tooltip(TileElement* tileElement)
|
||
{
|
||
auto rideIndex = tileElement->AsTrack()->GetRideIndex();
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride != nullptr)
|
||
{
|
||
auto ft = Formatter();
|
||
ft.Add<rct_string_id>(STR_RIDE_MAP_TIP);
|
||
ride->FormatNameTo(ft);
|
||
ride->FormatStatusTo(ft);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.putExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
context_broadcast_intent(&intent);
|
||
}
|
||
}
|
||
|
||
static void ride_queue_banner_set_map_tooltip(TileElement* tileElement)
|
||
{
|
||
auto rideIndex = tileElement->AsPath()->GetRideIndex();
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride != nullptr)
|
||
{
|
||
auto ft = Formatter();
|
||
ft.Add<rct_string_id>(STR_RIDE_MAP_TIP);
|
||
ride->FormatNameTo(ft);
|
||
ride->FormatStatusTo(ft);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.putExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
context_broadcast_intent(&intent);
|
||
}
|
||
}
|
||
|
||
static void ride_station_set_map_tooltip(TileElement* tileElement)
|
||
{
|
||
auto rideIndex = tileElement->AsTrack()->GetRideIndex();
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride != nullptr)
|
||
{
|
||
auto stationIndex = tileElement->AsTrack()->GetStationIndex();
|
||
for (int32_t i = stationIndex; i >= 0; i--)
|
||
if (ride->stations[i].Start.IsNull())
|
||
stationIndex--;
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<rct_string_id>(STR_RIDE_MAP_TIP);
|
||
ft.Add<rct_string_id>(ride->num_stations <= 1 ? STR_RIDE_STATION : STR_RIDE_STATION_X);
|
||
ride->FormatNameTo(ft);
|
||
ft.Add<rct_string_id>(GetRideComponentName(ride->GetRideTypeDescriptor().NameConvention.station).capitalised);
|
||
ft.Add<uint16_t>(stationIndex + 1);
|
||
ride->FormatStatusTo(ft);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.putExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
context_broadcast_intent(&intent);
|
||
}
|
||
}
|
||
|
||
static void ride_entrance_set_map_tooltip(TileElement* tileElement)
|
||
{
|
||
auto rideIndex = tileElement->AsEntrance()->GetRideIndex();
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride != nullptr)
|
||
{
|
||
// Get the station
|
||
auto stationIndex = tileElement->AsEntrance()->GetStationIndex();
|
||
for (int32_t i = stationIndex; i >= 0; i--)
|
||
if (ride->stations[i].Start.IsNull())
|
||
stationIndex--;
|
||
|
||
if (tileElement->AsEntrance()->GetEntranceType() == ENTRANCE_TYPE_RIDE_ENTRANCE)
|
||
{
|
||
// Get the queue length
|
||
int32_t queueLength = 0;
|
||
if (!ride_get_entrance_location(ride, stationIndex).IsNull())
|
||
queueLength = ride->stations[stationIndex].QueueLength;
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<rct_string_id>(STR_RIDE_MAP_TIP);
|
||
ft.Add<rct_string_id>(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));
|
||
|
||
ft.Add<uint16_t>(stationIndex + 1);
|
||
if (queueLength == 0)
|
||
{
|
||
ft.Add<rct_string_id>(STR_QUEUE_EMPTY);
|
||
}
|
||
else if (queueLength == 1)
|
||
{
|
||
ft.Add<rct_string_id>(STR_QUEUE_ONE_PERSON);
|
||
}
|
||
else
|
||
{
|
||
ft.Add<rct_string_id>(STR_QUEUE_PEOPLE);
|
||
}
|
||
ft.Add<uint16_t>(queueLength);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.putExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
context_broadcast_intent(&intent);
|
||
}
|
||
else
|
||
{
|
||
// Get the station
|
||
stationIndex = tileElement->AsEntrance()->GetStationIndex();
|
||
for (int32_t i = stationIndex; i >= 0; i--)
|
||
if (ride->stations[i].Start.IsNull())
|
||
stationIndex--;
|
||
|
||
auto ft = Formatter();
|
||
ft.Add<rct_string_id>(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));
|
||
|
||
ft.Add<uint16_t>(stationIndex + 1);
|
||
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
||
intent.putExtra(INTENT_EXTRA_FORMATTER, &ft);
|
||
context_broadcast_intent(&intent);
|
||
}
|
||
}
|
||
}
|
||
|
||
void ride_set_map_tooltip(TileElement* tileElement)
|
||
{
|
||
if (tileElement->GetType() == TILE_ELEMENT_TYPE_ENTRANCE)
|
||
{
|
||
ride_entrance_set_map_tooltip(tileElement);
|
||
}
|
||
else if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK)
|
||
{
|
||
if (tileElement->AsTrack()->IsStation())
|
||
{
|
||
ride_station_set_map_tooltip(tileElement);
|
||
}
|
||
else
|
||
{
|
||
ride_track_set_map_tooltip(tileElement);
|
||
}
|
||
}
|
||
else if (tileElement->GetType() == TILE_ELEMENT_TYPE_PATH)
|
||
{
|
||
ride_queue_banner_set_map_tooltip(tileElement);
|
||
}
|
||
}
|
||
|
||
#pragma endregion
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4CC1
|
||
*/
|
||
static StationIndex ride_mode_check_valid_station_numbers(Ride* ride)
|
||
{
|
||
uint16_t numStations = 0;
|
||
for (StationIndex stationIndex = 0; stationIndex < MAX_STATIONS; ++stationIndex)
|
||
{
|
||
if (!ride->stations[stationIndex].Start.IsNull())
|
||
{
|
||
numStations++;
|
||
}
|
||
}
|
||
|
||
switch (ride->mode)
|
||
{
|
||
case RideMode::ReverseInclineLaunchedShuttle:
|
||
case RideMode::PoweredLaunchPasstrough:
|
||
case RideMode::PoweredLaunch:
|
||
case RideMode::LimPoweredLaunch:
|
||
if (numStations <= 1)
|
||
return 1;
|
||
gGameCommandErrorText = STR_UNABLE_TO_OPERATE_WITH_MORE_THAN_ONE_STATION_IN_THIS_MODE;
|
||
return 0;
|
||
case RideMode::Shuttle:
|
||
if (numStations >= 2)
|
||
return 1;
|
||
gGameCommandErrorText = STR_UNABLE_TO_OPERATE_WITH_LESS_THAN_TWO_STATIONS_IN_THIS_MODE;
|
||
return 0;
|
||
default:
|
||
{
|
||
// This is workaround for multiple compilation errors of type "enumeration value ‘RIDE_MODE_*' not handled
|
||
// in switch [-Werror=switch]"
|
||
}
|
||
}
|
||
|
||
if (ride->type == RIDE_TYPE_GO_KARTS || ride->type == RIDE_TYPE_MINI_GOLF)
|
||
{
|
||
if (numStations <= 1)
|
||
return 1;
|
||
gGameCommandErrorText = STR_UNABLE_TO_OPERATE_WITH_MORE_THAN_ONE_STATION_IN_THIS_MODE;
|
||
return 0;
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* returns stationIndex of first station on success
|
||
* STATION_INDEX_NULL on failure.
|
||
*/
|
||
static StationIndex ride_mode_check_station_present(Ride* ride)
|
||
{
|
||
auto stationIndex = ride_get_first_valid_station_start(ride);
|
||
|
||
if (stationIndex == STATION_INDEX_NULL)
|
||
{
|
||
gGameCommandErrorText = STR_NOT_YET_CONSTRUCTED;
|
||
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_NO_TRACK))
|
||
return STATION_INDEX_NULL;
|
||
|
||
if (ride->type == RIDE_TYPE_MAZE)
|
||
return STATION_INDEX_NULL;
|
||
|
||
gGameCommandErrorText = STR_REQUIRES_A_STATION_PLATFORM;
|
||
return STATION_INDEX_NULL;
|
||
}
|
||
|
||
return stationIndex;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B5872
|
||
*/
|
||
static int32_t ride_check_for_entrance_exit(ride_id_t rideIndex)
|
||
{
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return 0;
|
||
|
||
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP))
|
||
return 1;
|
||
|
||
uint8_t entrance = 0;
|
||
uint8_t exit = 0;
|
||
for (int32_t i = 0; i < MAX_STATIONS; i++)
|
||
{
|
||
if (ride->stations[i].Start.IsNull())
|
||
continue;
|
||
|
||
if (!ride_get_entrance_location(ride, i).IsNull())
|
||
{
|
||
entrance = 1;
|
||
}
|
||
|
||
if (!ride_get_exit_location(ride, i).IsNull())
|
||
{
|
||
exit = 1;
|
||
}
|
||
|
||
// If station start and no entrance/exit
|
||
// Sets same error message as no entrance
|
||
if (ride_get_exit_location(ride, i).IsNull() && ride_get_entrance_location(ride, i).IsNull())
|
||
{
|
||
entrance = 0;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (entrance == 0)
|
||
{
|
||
gGameCommandErrorText = STR_ENTRANCE_NOT_YET_BUILT;
|
||
return 0;
|
||
}
|
||
|
||
if (exit == 0)
|
||
{
|
||
gGameCommandErrorText = STR_EXIT_NOT_YET_BUILT;
|
||
return 0;
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* Calls footpath_chain_ride_queue for all entrances of the ride
|
||
* rct2: 0x006B5952
|
||
*/
|
||
void Ride::ChainQueues() const
|
||
{
|
||
for (int32_t i = 0; i < MAX_STATIONS; i++)
|
||
{
|
||
auto location = ride_get_entrance_location(this, i);
|
||
if (location.IsNull())
|
||
continue;
|
||
|
||
auto mapLocation = location.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 = map_get_first_element_at(location);
|
||
if (tileElement != nullptr)
|
||
{
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_ENTRANCE)
|
||
continue;
|
||
if (tileElement->GetBaseZ() != mapLocation.z)
|
||
continue;
|
||
|
||
int32_t direction = tileElement->GetDirection();
|
||
footpath_chain_ride_queue(id, i, mapLocation, tileElement, direction_reverse(direction));
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006D3319
|
||
*/
|
||
static int32_t ride_check_block_brakes(CoordsXYE* input, CoordsXYE* output)
|
||
{
|
||
ride_id_t rideIndex = input->element->AsTrack()->GetRideIndex();
|
||
rct_window* w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && _currentRideIndex == rideIndex)
|
||
ride_construction_invalidate_current_track();
|
||
|
||
track_circuit_iterator it;
|
||
track_circuit_iterator_begin(&it, *input);
|
||
while (track_circuit_iterator_next(&it))
|
||
{
|
||
if (it.current.element->AsTrack()->GetTrackType() == TrackElemType::BlockBrakes)
|
||
{
|
||
auto type = it.last.element->AsTrack()->GetTrackType();
|
||
if (type == TrackElemType::EndStation)
|
||
{
|
||
gGameCommandErrorText = STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_STATION;
|
||
*output = it.current;
|
||
return 0;
|
||
}
|
||
if (type == TrackElemType::BlockBrakes)
|
||
{
|
||
gGameCommandErrorText = STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_EACH_OTHER;
|
||
*output = it.current;
|
||
return 0;
|
||
}
|
||
if (it.last.element->AsTrack()->HasChain() && type != TrackElemType::LeftCurvedLiftHill
|
||
&& type != TrackElemType::RightCurvedLiftHill)
|
||
{
|
||
gGameCommandErrorText = STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_THE_TOP_OF_THIS_LIFT_HILL;
|
||
*output = it.current;
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
if (!it.looped)
|
||
{
|
||
// Not sure why this is the case...
|
||
gGameCommandErrorText = STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_STATION;
|
||
*output = it.last;
|
||
return 0;
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* 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 ride_check_track_contains_inversions(CoordsXYE* input, CoordsXYE* output)
|
||
{
|
||
if (input->element == nullptr)
|
||
return false;
|
||
|
||
const auto* trackElement = input->element->AsTrack();
|
||
if (trackElement == nullptr)
|
||
return false;
|
||
|
||
ride_id_t rideIndex = trackElement->GetRideIndex();
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride != nullptr && ride->type == RIDE_TYPE_MAZE)
|
||
return true;
|
||
|
||
rct_window* w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && rideIndex == _currentRideIndex)
|
||
{
|
||
ride_construction_invalidate_current_track();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
track_circuit_iterator it, slowIt;
|
||
track_circuit_iterator_begin(&it, *input);
|
||
slowIt = it;
|
||
|
||
while (track_circuit_iterator_next(&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)
|
||
{
|
||
track_circuit_iterator_next(&slowIt);
|
||
if (track_circuit_iterators_match(&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 ride_check_track_contains_banked(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 = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
if (ride->type == RIDE_TYPE_MAZE)
|
||
return true;
|
||
|
||
rct_window* w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && rideIndex == _currentRideIndex)
|
||
{
|
||
ride_construction_invalidate_current_track();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
track_circuit_iterator it, slowIt;
|
||
track_circuit_iterator_begin(&it, *input);
|
||
slowIt = it;
|
||
|
||
while (track_circuit_iterator_next(&it))
|
||
{
|
||
auto trackType = output->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)
|
||
{
|
||
track_circuit_iterator_next(&slowIt);
|
||
if (track_circuit_iterators_match(&it, &slowIt))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006CB25D
|
||
*/
|
||
static int32_t ride_check_station_length(CoordsXYE* input, CoordsXYE* output)
|
||
{
|
||
rct_window* w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0
|
||
&& _currentRideIndex == input->element->AsTrack()->GetRideIndex())
|
||
{
|
||
ride_construction_invalidate_current_track();
|
||
}
|
||
|
||
output->x = input->x;
|
||
output->y = input->y;
|
||
output->element = input->element;
|
||
track_begin_end trackBeginEnd;
|
||
while (track_block_get_previous(*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 (ted.SequenceProperties[0] & 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 (track_block_get_next(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 ride_check_start_and_end_is_station(CoordsXYE* input)
|
||
{
|
||
CoordsXYE trackBack, trackFront;
|
||
|
||
ride_id_t rideIndex = input->element->AsTrack()->GetRideIndex();
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
auto w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && rideIndex == _currentRideIndex)
|
||
{
|
||
ride_construction_invalidate_current_track();
|
||
}
|
||
|
||
// Check back of the track
|
||
track_get_back(input, &trackBack);
|
||
auto trackType = trackBack.element->AsTrack()->GetTrackType();
|
||
const auto* ted = &GetTrackElementDescriptor(trackType);
|
||
if (!(ted->SequenceProperties[0] & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
{
|
||
return false;
|
||
}
|
||
ride->ChairliftBullwheelLocation[0] = TileCoordsXYZ{ CoordsXYZ{ trackBack.x, trackBack.y, trackBack.element->GetBaseZ() } };
|
||
|
||
// Check front of the track
|
||
track_get_front(input, &trackFront);
|
||
trackType = trackFront.element->AsTrack()->GetTrackType();
|
||
ted = &GetTrackElementDescriptor(trackType);
|
||
if (!(ted->SequenceProperties[0] & 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 ride_set_boat_hire_return_point(Ride* ride, CoordsXYE* startElement)
|
||
{
|
||
int32_t trackType = -1;
|
||
auto returnPos = *startElement;
|
||
int32_t startX = returnPos.x;
|
||
int32_t startY = returnPos.y;
|
||
track_begin_end trackBeginEnd;
|
||
while (track_block_get_previous(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 ride_set_maze_entrance_exit_points(Ride* ride)
|
||
{
|
||
// Needs room for an entrance and an exit per station, plus one position for the list terminator.
|
||
TileCoordsXYZD positions[(MAX_STATIONS * 2) + 1];
|
||
|
||
// Create a list of all the entrance and exit positions
|
||
TileCoordsXYZD* position = positions;
|
||
for (int32_t i = 0; i < MAX_STATIONS; i++)
|
||
{
|
||
const auto entrance = ride_get_entrance_location(ride, i);
|
||
const auto exit = ride_get_exit_location(ride, i);
|
||
|
||
if (!entrance.IsNull())
|
||
{
|
||
*position++ = entrance;
|
||
}
|
||
if (!exit.IsNull())
|
||
{
|
||
*position++ = exit;
|
||
}
|
||
}
|
||
(*position++).SetNull();
|
||
|
||
// Enumerate entrance and exit positions
|
||
for (position = positions; !(*position).IsNull(); position++)
|
||
{
|
||
auto entranceExitMapPos = position->ToCoordsXYZ();
|
||
|
||
TileElement* tileElement = map_get_first_element_at(*position);
|
||
do
|
||
{
|
||
if (tileElement == nullptr)
|
||
break;
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_ENTRANCE)
|
||
continue;
|
||
if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_ENTRANCE
|
||
&& tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_RIDE_EXIT)
|
||
{
|
||
continue;
|
||
}
|
||
if (tileElement->GetBaseZ() != entranceExitMapPos.z)
|
||
continue;
|
||
|
||
maze_entrance_hedge_removal({ entranceExitMapPos, tileElement });
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Opens all block brakes of a ride.
|
||
* rct2: 0x006B4E6B
|
||
*/
|
||
static void RideOpenBlockBrakes(CoordsXYE* startElement)
|
||
{
|
||
CoordsXYE currentElement = *startElement;
|
||
do
|
||
{
|
||
auto trackType = currentElement.element->AsTrack()->GetTrackType();
|
||
switch (trackType)
|
||
{
|
||
case TrackElemType::EndStation:
|
||
case TrackElemType::CableLiftHill:
|
||
case TrackElemType::Up25ToFlat:
|
||
case TrackElemType::Up60ToFlat:
|
||
case TrackElemType::DiagUp25ToFlat:
|
||
case TrackElemType::DiagUp60ToFlat:
|
||
case TrackElemType::BlockBrakes:
|
||
currentElement.element->AsTrack()->SetBlockBrakeClosed(false);
|
||
break;
|
||
}
|
||
} while (track_block_get_next(¤tElement, ¤tElement, nullptr, nullptr)
|
||
&& currentElement.element != startElement->element);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4D26
|
||
*/
|
||
static void ride_set_start_finish_points(ride_id_t rideIndex, CoordsXYE* startElement)
|
||
{
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return;
|
||
|
||
switch (ride->type)
|
||
{
|
||
case RIDE_TYPE_BOAT_HIRE:
|
||
ride_set_boat_hire_return_point(ride, startElement);
|
||
break;
|
||
case RIDE_TYPE_MAZE:
|
||
ride_set_maze_entrance_exit_points(ride);
|
||
break;
|
||
}
|
||
|
||
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 const CoordsXY word_9A3AB4[4] = {
|
||
{ 0, 0 },
|
||
{ 0, -96 },
|
||
{ -96, -96 },
|
||
{ -96, 0 },
|
||
};
|
||
|
||
// clang-format off
|
||
static constexpr const 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* vehicle_create_car(
|
||
ride_id_t rideIndex, int32_t vehicleEntryIndex, int32_t carIndex, int32_t vehicleIndex, const CoordsXYZ& carPosition,
|
||
int32_t* remainingDistance, TrackElement* trackElement)
|
||
{
|
||
if (trackElement == nullptr)
|
||
return nullptr;
|
||
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return nullptr;
|
||
|
||
auto rideEntry = ride->GetRideEntry();
|
||
if (rideEntry == nullptr)
|
||
return nullptr;
|
||
|
||
auto vehicleEntry = &rideEntry->vehicles[vehicleEntryIndex];
|
||
auto vehicle = CreateEntity<Vehicle>();
|
||
if (vehicle == nullptr)
|
||
return nullptr;
|
||
|
||
vehicle->ride = rideIndex;
|
||
vehicle->ride_subtype = ride->subtype;
|
||
|
||
vehicle->vehicle_type = vehicleEntryIndex;
|
||
vehicle->SubType = carIndex == 0 ? Vehicle::Type::Head : Vehicle::Type::Tail;
|
||
vehicle->var_44 = Numerics::ror32(vehicleEntry->spacing, 10) & 0xFFFF;
|
||
auto edx = vehicleEntry->spacing >> 1;
|
||
*remainingDistance -= edx;
|
||
vehicle->remaining_distance = *remainingDistance;
|
||
if (!(vehicleEntry->flags & VEHICLE_ENTRY_FLAG_GO_KART))
|
||
{
|
||
*remainingDistance -= edx;
|
||
}
|
||
|
||
// loc_6DD9A5:
|
||
vehicle->sprite_width = vehicleEntry->sprite_width;
|
||
vehicle->sprite_height_negative = vehicleEntry->sprite_height_negative;
|
||
vehicle->sprite_height_positive = vehicleEntry->sprite_height_positive;
|
||
vehicle->mass = vehicleEntry->car_mass;
|
||
vehicle->num_seats = vehicleEntry->num_seats;
|
||
vehicle->speed = vehicleEntry->powered_max_speed;
|
||
vehicle->powered_acceleration = vehicleEntry->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 = SPRITE_INDEX_NULL;
|
||
vehicle->var_C4 = 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 (int32_t i = 0; i < 32; i++)
|
||
{
|
||
vehicle->peep[i] = SPRITE_INDEX_NULL;
|
||
}
|
||
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_DODGEM_CAR_PLACEMENT)
|
||
{
|
||
// loc_6DDCA4:
|
||
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 += ride->GetRideTypeDescriptor().Heights.VehicleZOffset;
|
||
|
||
vehicle->SetTrackDirection(0);
|
||
vehicle->SetTrackType(trackElement->GetTrackType());
|
||
vehicle->track_progress = 0;
|
||
vehicle->SetState(Vehicle::Status::MovingToEndOfStation);
|
||
vehicle->update_flags = 0;
|
||
|
||
CoordsXY chosenLoc;
|
||
auto numAttempts = 0;
|
||
// loc_6DDD26:
|
||
do
|
||
{
|
||
numAttempts++;
|
||
// This can happen when trying to spawn dozens of cars in a tiny area.
|
||
if (numAttempts > 10000)
|
||
return nullptr;
|
||
|
||
vehicle->sprite_direction = scenario_rand() & 0x1E;
|
||
chosenLoc.y = dodgemPos.y + (scenario_rand() & 0xFF);
|
||
chosenLoc.x = dodgemPos.x + (scenario_rand() & 0xFF);
|
||
} while (vehicle->DodgemsCarWouldCollideAt(chosenLoc, nullptr));
|
||
|
||
vehicle->MoveTo({ chosenLoc, dodgemPos.z });
|
||
}
|
||
else
|
||
{
|
||
VehicleTrackSubposition subposition = VehicleTrackSubposition::Default;
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_CHAIRLIFT)
|
||
{
|
||
subposition = VehicleTrackSubposition::ChairliftGoingOut;
|
||
}
|
||
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_GO_KART)
|
||
{
|
||
// Choose which lane Go Kart should start in
|
||
subposition = VehicleTrackSubposition::GoKartsLeftLane;
|
||
if (vehicleIndex & 1)
|
||
{
|
||
subposition = VehicleTrackSubposition::GoKartsRightLane;
|
||
}
|
||
}
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_MINI_GOLF)
|
||
{
|
||
subposition = VehicleTrackSubposition::MiniGolfStart9;
|
||
vehicle->var_D3 = 0;
|
||
vehicle->mini_golf_current_animation = MiniGolfAnimation::Walk;
|
||
vehicle->mini_golf_flags = 0;
|
||
}
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_REVERSER_BOGIE)
|
||
{
|
||
if (vehicle->IsHead())
|
||
{
|
||
subposition = VehicleTrackSubposition::ReverserRCFrontBogie;
|
||
}
|
||
}
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_REVERSER_PASSENGER_CAR)
|
||
{
|
||
subposition = VehicleTrackSubposition::ReverserRCRearBogie;
|
||
}
|
||
vehicle->TrackSubposition = subposition;
|
||
|
||
auto chosenLoc = carPosition;
|
||
vehicle->TrackLocation = chosenLoc;
|
||
|
||
int32_t direction = trackElement->GetDirection();
|
||
vehicle->sprite_direction = direction << 3;
|
||
|
||
if (ride->type == RIDE_TYPE_SPACE_RINGS)
|
||
{
|
||
direction = 4;
|
||
}
|
||
else
|
||
{
|
||
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_VEHICLE_IS_INTEGRAL))
|
||
{
|
||
if (ride->GetRideTypeDescriptor().StartTrackPiece != TrackElemType::FlatTrack1x4B)
|
||
{
|
||
if (ride->GetRideTypeDescriptor().StartTrackPiece != TrackElemType::FlatTrack1x4A)
|
||
{
|
||
if (ride->type == RIDE_TYPE_ENTERPRISE)
|
||
{
|
||
direction += 5;
|
||
}
|
||
else
|
||
{
|
||
direction = 4;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
chosenLoc += CoordsXYZ{ word_9A2A60[direction], ride->GetRideTypeDescriptor().Heights.VehicleZOffset };
|
||
|
||
vehicle->current_station = trackElement->GetStationIndex();
|
||
|
||
vehicle->MoveTo(chosenLoc);
|
||
vehicle->SetTrackType(trackElement->GetTrackType());
|
||
vehicle->SetTrackDirection(vehicle->sprite_direction >> 3);
|
||
vehicle->track_progress = 31;
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_MINI_GOLF)
|
||
{
|
||
vehicle->track_progress = 15;
|
||
}
|
||
vehicle->update_flags = VEHICLE_UPDATE_FLAG_COLLISION_DISABLED;
|
||
if (vehicleEntry->flags & VEHICLE_ENTRY_FLAG_HAS_INVERTED_SPRITE_SET)
|
||
{
|
||
if (trackElement->IsInverted())
|
||
{
|
||
vehicle->SetUpdateFlag(VEHICLE_UPDATE_FLAG_USE_INVERTED_SPRITES);
|
||
}
|
||
}
|
||
vehicle->SetState(Vehicle::Status::MovingToEndOfStation);
|
||
}
|
||
|
||
// loc_6DDD5E:
|
||
vehicle->num_peeps = 0;
|
||
vehicle->next_free_seat = 0;
|
||
vehicle->BoatLocation.SetNull();
|
||
vehicle->IsCrashedVehicle = false;
|
||
return vehicle;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DD84C
|
||
*/
|
||
static train_ref vehicle_create_train(
|
||
ride_id_t rideIndex, const CoordsXYZ& trainPos, int32_t vehicleIndex, int32_t* remainingDistance,
|
||
TrackElement* trackElement)
|
||
{
|
||
train_ref train = { nullptr, nullptr };
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride != nullptr)
|
||
{
|
||
for (int32_t carIndex = 0; carIndex < ride->num_cars_per_train; carIndex++)
|
||
{
|
||
auto vehicle = ride_entry_get_vehicle_at_position(ride->subtype, ride->num_cars_per_train, carIndex);
|
||
auto car = vehicle_create_car(
|
||
rideIndex, vehicle, carIndex, 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->sprite_index;
|
||
train.tail->next_vehicle_on_ride = car->sprite_index;
|
||
car->prev_vehicle_on_ride = train.tail->sprite_index;
|
||
}
|
||
train.tail = car;
|
||
}
|
||
}
|
||
return train;
|
||
}
|
||
|
||
static bool vehicle_create_trains(ride_id_t rideIndex, const CoordsXYZ& trainsPos, TrackElement* trackElement)
|
||
{
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
train_ref firstTrain = {};
|
||
train_ref lastTrain = {};
|
||
int32_t remainingDistance = 0;
|
||
bool allTrainsCreated = true;
|
||
|
||
for (int32_t vehicleIndex = 0; vehicleIndex < ride->num_vehicles; vehicleIndex++)
|
||
{
|
||
if (ride->IsBlockSectioned())
|
||
{
|
||
remainingDistance = 0;
|
||
}
|
||
train_ref train = vehicle_create_train(rideIndex, 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->sprite_index;
|
||
train.head->prev_vehicle_on_ride = lastTrain.tail->sprite_index;
|
||
}
|
||
lastTrain = train;
|
||
|
||
for (int32_t i = 0; i <= MAX_VEHICLES_PER_RIDE; i++)
|
||
{
|
||
if (ride->vehicles[i] == SPRITE_INDEX_NULL)
|
||
{
|
||
ride->vehicles[i] = train.head->sprite_index;
|
||
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->sprite_index;
|
||
if (firstTrain.head != nullptr)
|
||
lastTrain.tail->next_vehicle_on_ride = firstTrain.head->sprite_index;
|
||
|
||
return allTrainsCreated;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DDE9E
|
||
*/
|
||
static void ride_create_vehicles_find_first_block(Ride* ride, CoordsXYE* outXYElement)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[0]);
|
||
if (vehicle == nullptr)
|
||
return;
|
||
|
||
auto curTrackPos = vehicle->TrackLocation;
|
||
auto curTrackElement = map_get_track_element_at(curTrackPos);
|
||
|
||
assert(curTrackElement != nullptr);
|
||
|
||
CoordsXY trackPos = curTrackPos;
|
||
auto trackElement = curTrackElement;
|
||
track_begin_end trackBeginEnd;
|
||
while (track_block_get_previous({ 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::Up25ToFlat:
|
||
case TrackElemType::Up60ToFlat:
|
||
if (trackElement->HasChain())
|
||
{
|
||
*outXYElement = { trackPos, reinterpret_cast<TileElement*>(trackElement) };
|
||
return;
|
||
}
|
||
break;
|
||
case TrackElemType::DiagUp25ToFlat:
|
||
case TrackElemType::DiagUp60ToFlat:
|
||
if (trackElement->HasChain())
|
||
{
|
||
TileElement* tileElement = map_get_track_element_at_of_type_seq(
|
||
{ 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::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
|
||
*/
|
||
bool 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 = num_vehicles * num_cars_per_train;
|
||
if (totalCars > count_free_misc_sprite_slots())
|
||
{
|
||
gGameCommandErrorText = STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES;
|
||
return false;
|
||
}
|
||
|
||
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 = map_get_track_element_at(vehiclePos);
|
||
|
||
vehiclePos.z = trackElement->GetBaseZ();
|
||
}
|
||
|
||
if (!vehicle_create_trains(id, vehiclePos, trackElement))
|
||
{
|
||
// This flag is needed for Ride::RemoveVehicles()
|
||
lifecycle_flags |= RIDE_LIFECYCLE_ON_TRACK;
|
||
RemoveVehicles();
|
||
gGameCommandErrorText = STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES;
|
||
return false;
|
||
}
|
||
// return true;
|
||
|
||
// Initialise station departs
|
||
// 006DDDD0:
|
||
lifecycle_flags |= RIDE_LIFECYCLE_ON_TRACK;
|
||
for (int32_t i = 0; i < MAX_STATIONS; i++)
|
||
{
|
||
stations[i].Depart = (stations[i].Depart & STATION_DEPART_FLAG) | 1;
|
||
}
|
||
|
||
//
|
||
if (type != RIDE_TYPE_SPACE_RINGS && !GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_VEHICLE_IS_INTEGRAL))
|
||
{
|
||
if (IsBlockSectioned())
|
||
{
|
||
CoordsXYE firstBlock{};
|
||
ride_create_vehicles_find_first_block(this, &firstBlock);
|
||
MoveTrainsToBlockBrakes(firstBlock.element->AsTrack());
|
||
}
|
||
else
|
||
{
|
||
for (int32_t i = 0; i < num_vehicles; i++)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(vehicles[i]);
|
||
if (vehicle == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
auto vehicleEntry = vehicle->Entry();
|
||
|
||
if (!(vehicleEntry->flags & VEHICLE_ENTRY_FLAG_DODGEM_CAR_PLACEMENT))
|
||
{
|
||
vehicle->UpdateTrackMotion(nullptr);
|
||
}
|
||
|
||
vehicle->EnableCollisionsForTrain();
|
||
}
|
||
}
|
||
}
|
||
ride_update_vehicle_colours(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(TrackElement* firstBlock)
|
||
{
|
||
for (int32_t i = 0; i < num_vehicles; i++)
|
||
{
|
||
auto train = GetEntity<Vehicle>(vehicles[i]);
|
||
if (train == nullptr)
|
||
continue;
|
||
|
||
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->SetBlockBrakeClosed(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));
|
||
|
||
firstBlock->SetBlockBrakeClosed(true);
|
||
for (Vehicle* car = train; car != nullptr; car = GetEntity<Vehicle>(car->next_vehicle_on_train))
|
||
{
|
||
car->ClearUpdateFlag(VEHICLE_UPDATE_FLAG_COLLISION_DISABLED);
|
||
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 bool ride_initialise_cable_lift_track(Ride* ride, bool isApplying)
|
||
{
|
||
CoordsXYZ location;
|
||
for (StationIndex stationIndex = 0; stationIndex < MAX_STATIONS; stationIndex++)
|
||
{
|
||
location = ride->stations[stationIndex].GetStart();
|
||
if (!location.IsNull())
|
||
break;
|
||
if (stationIndex == (MAX_STATIONS - 1))
|
||
{
|
||
gGameCommandErrorText = STR_CABLE_LIFT_HILL_MUST_START_IMMEDIATELY_AFTER_STATION;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool success = false;
|
||
TileElement* tileElement = map_get_first_element_at(location);
|
||
if (tileElement == nullptr)
|
||
return success;
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
|
||
continue;
|
||
if (tileElement->GetBaseZ() != location.z)
|
||
continue;
|
||
|
||
const auto& ted = GetTrackElementDescriptor(tileElement->AsTrack()->GetTrackType());
|
||
if (!(ted.SequenceProperties[0] & 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;
|
||
|
||
track_circuit_iterator it;
|
||
track_circuit_iterator_begin(&it, { location, tileElement });
|
||
while (track_circuit_iterator_previous(&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:
|
||
gGameCommandErrorText = STR_CABLE_LIFT_HILL_MUST_START_IMMEDIATELY_AFTER_STATION;
|
||
return false;
|
||
}
|
||
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 bool ride_create_cable_lift(ride_id_t rideIndex, bool isApplying)
|
||
{
|
||
auto ride = get_ride(rideIndex);
|
||
if (ride == nullptr)
|
||
return false;
|
||
|
||
if (ride->mode != RideMode::ContinuousCircuitBlockSectioned && ride->mode != RideMode::ContinuousCircuit)
|
||
{
|
||
gGameCommandErrorText = STR_CABLE_LIFT_UNABLE_TO_WORK_IN_THIS_OPERATING_MODE;
|
||
return false;
|
||
}
|
||
|
||
if (ride->num_circuits > 1)
|
||
{
|
||
gGameCommandErrorText = STR_MULTICIRCUIT_NOT_POSSIBLE_WITH_CABLE_LIFT_HILL;
|
||
return false;
|
||
}
|
||
|
||
if (count_free_misc_sprite_slots() <= 5)
|
||
{
|
||
gGameCommandErrorText = STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES;
|
||
return false;
|
||
}
|
||
|
||
if (!ride_initialise_cable_lift_track(ride, isApplying))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!isApplying)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
auto cableLiftLoc = ride->CableLiftLoc;
|
||
auto tileElement = map_get_track_element_at(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 = cable_lift_segment_create(
|
||
*ride, cableLiftLoc.x, cableLiftLoc.y, cableLiftLoc.z / 8, direction, var_44, remaining_distance, i == 0);
|
||
current->next_vehicle_on_train = SPRITE_INDEX_NULL;
|
||
if (i == 0)
|
||
{
|
||
head = current;
|
||
}
|
||
else
|
||
{
|
||
tail->next_vehicle_on_train = current->sprite_index;
|
||
tail->next_vehicle_on_ride = current->sprite_index;
|
||
current->prev_vehicle_on_ride = tail->sprite_index;
|
||
}
|
||
tail = current;
|
||
}
|
||
head->prev_vehicle_on_ride = tail->sprite_index;
|
||
tail->next_vehicle_on_ride = head->sprite_index;
|
||
|
||
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 the screen to the first station missing the entrance or exit.
|
||
* rct2: 0x006B51C0
|
||
*/
|
||
void Ride::ConstructMissingEntranceOrExit() const
|
||
{
|
||
auto* w = window_get_main();
|
||
if (w == nullptr)
|
||
return;
|
||
|
||
int8_t entranceOrExit = -1;
|
||
int32_t i;
|
||
for (i = 0; i < MAX_STATIONS; i++)
|
||
{
|
||
if (stations[i].Start.IsNull())
|
||
continue;
|
||
|
||
if (ride_get_entrance_location(this, i).IsNull())
|
||
{
|
||
entranceOrExit = WC_RIDE_CONSTRUCTION__WIDX_ENTRANCE;
|
||
break;
|
||
}
|
||
|
||
if (ride_get_exit_location(this, i).IsNull())
|
||
{
|
||
entranceOrExit = WC_RIDE_CONSTRUCTION__WIDX_EXIT;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (entranceOrExit == -1)
|
||
return;
|
||
|
||
if (type != RIDE_TYPE_MAZE)
|
||
{
|
||
auto location = stations[i].GetStart();
|
||
window_scroll_to_location(w, location);
|
||
|
||
CoordsXYE trackElement;
|
||
ride_try_get_origin_element(this, &trackElement);
|
||
ride_find_track_gap(this, &trackElement, &trackElement);
|
||
int32_t ok = ride_modify(&trackElement);
|
||
if (ok == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr)
|
||
window_event_mouse_up_call(w, entranceOrExit);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B528A
|
||
*/
|
||
static void ride_scroll_to_track_error(CoordsXYE* trackElement)
|
||
{
|
||
auto* w = window_get_main();
|
||
if (w != nullptr)
|
||
{
|
||
window_scroll_to_location(w, { *trackElement, trackElement->element->GetBaseZ() });
|
||
ride_modify(trackElement);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B4F6B
|
||
*/
|
||
TrackElement* Ride::GetOriginElement(StationIndex stationIndex) const
|
||
{
|
||
auto stationLoc = stations[stationIndex].Start;
|
||
TileElement* tileElement = map_get_first_element_at(stationLoc);
|
||
if (tileElement == nullptr)
|
||
return nullptr;
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
|
||
continue;
|
||
|
||
auto* trackElement = tileElement->AsTrack();
|
||
const auto& ted = GetTrackElementDescriptor(trackElement->GetTrackType());
|
||
if (!(ted.SequenceProperties[0] & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
continue;
|
||
|
||
if (trackElement->GetRideIndex() == id)
|
||
return trackElement;
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
bool Ride::Test(RideStatus newStatus, bool isApplying)
|
||
{
|
||
CoordsXYE trackElement, problematicTrackElement = {};
|
||
|
||
if (type == RIDE_TYPE_NULL)
|
||
{
|
||
log_warning("Invalid ride type for ride %u", EnumValue(id));
|
||
return false;
|
||
}
|
||
|
||
if (newStatus != RideStatus::Simulating)
|
||
{
|
||
window_close_by_number(WC_RIDE_CONSTRUCTION, EnumValue(id));
|
||
}
|
||
|
||
StationIndex stationIndex = ride_mode_check_station_present(this);
|
||
if (stationIndex == STATION_INDEX_NULL)
|
||
return false;
|
||
|
||
if (!ride_mode_check_valid_station_numbers(this))
|
||
return false;
|
||
|
||
if (newStatus != RideStatus::Simulating && !ride_check_for_entrance_exit(id))
|
||
{
|
||
ConstructMissingEntranceOrExit();
|
||
return false;
|
||
}
|
||
|
||
// z = ride->stations[i].GetBaseZ();
|
||
auto startLoc = stations[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
|
||
if (type != RIDE_TYPE_MAZE)
|
||
return false;
|
||
}
|
||
|
||
if (mode == RideMode::ContinuousCircuit || IsBlockSectioned())
|
||
{
|
||
if (ride_find_track_gap(this, &trackElement, &problematicTrackElement)
|
||
&& (newStatus != RideStatus::Simulating || IsBlockSectioned()))
|
||
{
|
||
gGameCommandErrorText = STR_TRACK_IS_NOT_A_COMPLETE_CIRCUIT;
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (IsBlockSectioned())
|
||
{
|
||
if (!ride_check_block_brakes(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (subtype != OBJECT_ENTRY_INDEX_NULL && !gCheatsEnableAllDrawableTrackPieces)
|
||
{
|
||
rct_ride_entry* rideType = get_ride_entry(subtype);
|
||
if (rideType->flags & RIDE_ENTRY_FLAG_NO_INVERSIONS)
|
||
{
|
||
gGameCommandErrorText = STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN;
|
||
if (ride_check_track_contains_inversions(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
if (rideType->flags & RIDE_ENTRY_FLAG_NO_BANKED_TRACK)
|
||
{
|
||
gGameCommandErrorText = STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN;
|
||
if (ride_check_track_contains_banked(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (mode == RideMode::StationToStation)
|
||
{
|
||
if (!ride_find_track_gap(this, &trackElement, &problematicTrackElement))
|
||
{
|
||
gGameCommandErrorText = STR_RIDE_MUST_START_AND_END_WITH_STATIONS;
|
||
return false;
|
||
}
|
||
|
||
gGameCommandErrorText = STR_STATION_NOT_LONG_ENOUGH;
|
||
if (!ride_check_station_length(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
|
||
gGameCommandErrorText = STR_RIDE_MUST_START_AND_END_WITH_STATIONS;
|
||
if (!ride_check_start_and_end_is_station(&trackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (isApplying)
|
||
ride_set_start_finish_points(id, &trackElement);
|
||
|
||
const auto& rtd = GetRideTypeDescriptor();
|
||
if (!rtd.HasFlag(RIDE_TYPE_FLAG_NO_VEHICLES) && !(lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK))
|
||
{
|
||
if (!CreateVehicles(trackElement, isApplying))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
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))
|
||
{
|
||
if (!ride_create_cable_lift(id, isApplying))
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
/**
|
||
*
|
||
* rct2: 0x006B4EEA
|
||
*/
|
||
bool Ride::Open(bool isApplying)
|
||
{
|
||
CoordsXYE trackElement, problematicTrackElement = {};
|
||
|
||
// 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 (WC_RIDE_CONSTRUCTION == gCurrentToolWidget.window_classification && EnumValue(id) == gCurrentToolWidget.window_number
|
||
&& (input_test_flag(INPUT_FLAG_TOOL_ACTIVE)))
|
||
{
|
||
window_close_by_number(WC_RIDE_CONSTRUCTION, EnumValue(id));
|
||
}
|
||
|
||
StationIndex stationIndex = ride_mode_check_station_present(this);
|
||
if (stationIndex == STATION_INDEX_NULL)
|
||
return false;
|
||
|
||
if (!ride_mode_check_valid_station_numbers(this))
|
||
return false;
|
||
|
||
if (!ride_check_for_entrance_exit(id))
|
||
{
|
||
ConstructMissingEntranceOrExit();
|
||
return false;
|
||
}
|
||
|
||
if (isApplying)
|
||
{
|
||
ChainQueues();
|
||
lifecycle_flags |= RIDE_LIFECYCLE_EVER_BEEN_OPENED;
|
||
}
|
||
|
||
// z = ride->stations[i].GetBaseZ();
|
||
auto startLoc = stations[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
|
||
if (type != RIDE_TYPE_MAZE)
|
||
return false;
|
||
}
|
||
|
||
if (mode == RideMode::Race || mode == RideMode::ContinuousCircuit || IsBlockSectioned())
|
||
{
|
||
if (ride_find_track_gap(this, &trackElement, &problematicTrackElement))
|
||
{
|
||
gGameCommandErrorText = STR_TRACK_IS_NOT_A_COMPLETE_CIRCUIT;
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (IsBlockSectioned())
|
||
{
|
||
if (!ride_check_block_brakes(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (subtype != OBJECT_ENTRY_INDEX_NULL && !gCheatsEnableAllDrawableTrackPieces)
|
||
{
|
||
rct_ride_entry* rideEntry = get_ride_entry(subtype);
|
||
if (rideEntry->flags & RIDE_ENTRY_FLAG_NO_INVERSIONS)
|
||
{
|
||
gGameCommandErrorText = STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN;
|
||
if (ride_check_track_contains_inversions(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
if (rideEntry->flags & RIDE_ENTRY_FLAG_NO_BANKED_TRACK)
|
||
{
|
||
gGameCommandErrorText = STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN;
|
||
if (ride_check_track_contains_banked(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (mode == RideMode::StationToStation)
|
||
{
|
||
if (!ride_find_track_gap(this, &trackElement, &problematicTrackElement))
|
||
{
|
||
gGameCommandErrorText = STR_RIDE_MUST_START_AND_END_WITH_STATIONS;
|
||
return false;
|
||
}
|
||
|
||
gGameCommandErrorText = STR_STATION_NOT_LONG_ENOUGH;
|
||
if (!ride_check_station_length(&trackElement, &problematicTrackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
|
||
gGameCommandErrorText = STR_RIDE_MUST_START_AND_END_WITH_STATIONS;
|
||
if (!ride_check_start_and_end_is_station(&trackElement))
|
||
{
|
||
ride_scroll_to_track_error(&problematicTrackElement);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (isApplying)
|
||
ride_set_start_finish_points(id, &trackElement);
|
||
|
||
if (!GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_NO_VEHICLES) && !(lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK))
|
||
{
|
||
if (!CreateVehicles(trackElement, isApplying))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if ((GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_ALLOW_CABLE_LIFT_HILL))
|
||
&& (lifecycle_flags & RIDE_LIFECYCLE_CABLE_LIFT_HILL_COMPONENT_USED) && !(lifecycle_flags & RIDE_LIFECYCLE_CABLE_LIFT))
|
||
{
|
||
if (!ride_create_cable_lift(id, isApplying))
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 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 ride_get_start_of_track(CoordsXYE* output)
|
||
{
|
||
track_begin_end trackBeginEnd;
|
||
CoordsXYE trackElement = *output;
|
||
if (track_block_get_previous(trackElement, &trackBeginEnd))
|
||
{
|
||
TileElement* initial_map = trackElement.element;
|
||
track_begin_end 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 (!track_block_get_previous(
|
||
{ trackBeginEnd.end_x, trackBeginEnd.end_y, trackBeginEnd.begin_element }, &trackBeginEnd))
|
||
{
|
||
trackElement = lastGood;
|
||
break;
|
||
}
|
||
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
if (!track_block_get_previous({ 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 ride_with_colour_config_exists(uint8_t ride_type, const TrackColour* colours)
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.type != ride_type)
|
||
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, ride_id_t excludeRideId)
|
||
{
|
||
char buffer[256]{};
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
if (ride.id != excludeRideId)
|
||
{
|
||
Formatter ft;
|
||
ride.FormatNameTo(ft);
|
||
format_string(buffer, 256, STR_STRINGID, ft.Data());
|
||
if (name == buffer && ride_has_any_track_elements(&ride))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Based on rct2: 0x006B4776
|
||
*/
|
||
int32_t ride_get_random_colour_preset_index(uint8_t ride_type)
|
||
{
|
||
if (ride_type >= std::size(RideTypeDescriptors))
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
const track_colour_preset_list* colourPresets = &GetRideTypeDescriptor(ride_type).ColourPresets;
|
||
|
||
// 200 attempts to find a colour preset that hasn't already been used in the park for this ride type
|
||
for (int32_t i = 0; i < 200; i++)
|
||
{
|
||
int32_t listIndex = util_rand() % colourPresets->count;
|
||
const TrackColour* colours = &colourPresets->list[listIndex];
|
||
|
||
if (!ride_with_colour_config_exists(ride_type, colours))
|
||
{
|
||
return listIndex;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Based on rct2: 0x006B4776
|
||
*/
|
||
void Ride::SetColourPreset(uint8_t index)
|
||
{
|
||
const track_colour_preset_list* 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())
|
||
{
|
||
auto rideEntry = get_ride_entry(subtype);
|
||
if (rideEntry != nullptr && rideEntry->vehicle_preset_list->count > 0)
|
||
{
|
||
auto list = rideEntry->vehicle_preset_list->list[0];
|
||
colours = { list.main, list.additional_1, list.additional_2 };
|
||
}
|
||
}
|
||
else if (index < colourPresets->count)
|
||
{
|
||
colours = colourPresets->list[index];
|
||
}
|
||
for (int32_t i = 0; i < NUM_COLOUR_SCHEMES; i++)
|
||
{
|
||
track_colour[i].main = colours.main;
|
||
track_colour[i].additional = colours.additional;
|
||
track_colour[i].supports = colours.supports;
|
||
}
|
||
colour_scheme_type = 0;
|
||
}
|
||
|
||
money32 ride_get_common_price(Ride* forRide)
|
||
{
|
||
for (const auto& ride : GetRideManager())
|
||
{
|
||
if (ride.type == forRide->type && &ride != forRide)
|
||
{
|
||
return ride.price[0];
|
||
}
|
||
}
|
||
|
||
return MONEY32_UNDEFINED;
|
||
}
|
||
|
||
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);
|
||
format_string(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 get_ride_naming(const uint8_t rideType, rct_ride_entry* rideEntry)
|
||
{
|
||
if (!GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
|
||
{
|
||
return GetRideTypeDescriptor(rideType).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 increment_turn_count_1_element(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 & TURN_MASK_1_ELEMENT) + 1;
|
||
*turn_count &= ~TURN_MASK_1_ELEMENT;
|
||
|
||
if (value > TURN_MASK_1_ELEMENT)
|
||
value = TURN_MASK_1_ELEMENT;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
void increment_turn_count_2_elements(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 & TURN_MASK_2_ELEMENTS) + 0x20;
|
||
*turn_count &= ~TURN_MASK_2_ELEMENTS;
|
||
|
||
if (value > TURN_MASK_2_ELEMENTS)
|
||
value = TURN_MASK_2_ELEMENTS;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
void increment_turn_count_3_elements(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 & TURN_MASK_3_ELEMENTS) + 0x100;
|
||
*turn_count &= ~TURN_MASK_3_ELEMENTS;
|
||
|
||
if (value > TURN_MASK_3_ELEMENTS)
|
||
value = TURN_MASK_3_ELEMENTS;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
void increment_turn_count_4_plus_elements(Ride* ride, uint8_t type)
|
||
{
|
||
uint16_t* turn_count;
|
||
switch (type)
|
||
{
|
||
case 0:
|
||
case 1:
|
||
// Just in case fallback to 3 element turn
|
||
increment_turn_count_3_elements(ride, type);
|
||
return;
|
||
case 2:
|
||
turn_count = &ride->turn_count_sloped;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
uint16_t value = (*turn_count & TURN_MASK_4_PLUS_ELEMENTS) + 0x800;
|
||
*turn_count &= ~TURN_MASK_4_PLUS_ELEMENTS;
|
||
|
||
if (value > TURN_MASK_4_PLUS_ELEMENTS)
|
||
value = TURN_MASK_4_PLUS_ELEMENTS;
|
||
*turn_count |= value;
|
||
}
|
||
|
||
int32_t get_turn_count_1_element(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 0;
|
||
}
|
||
|
||
return (*turn_count) & TURN_MASK_1_ELEMENT;
|
||
}
|
||
|
||
int32_t get_turn_count_2_elements(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 0;
|
||
}
|
||
|
||
return ((*turn_count) & TURN_MASK_2_ELEMENTS) >> 5;
|
||
}
|
||
|
||
int32_t get_turn_count_3_elements(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 0;
|
||
}
|
||
|
||
return ((*turn_count) & TURN_MASK_3_ELEMENTS) >> 8;
|
||
}
|
||
|
||
int32_t get_turn_count_4_plus_elements(Ride* ride, uint8_t type)
|
||
{
|
||
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) & TURN_MASK_4_PLUS_ELEMENTS) >> 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 ride_get_helix_sections(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 ride_has_any_track_elements(const Ride* ride)
|
||
{
|
||
tile_element_iterator it;
|
||
|
||
tile_element_iterator_begin(&it);
|
||
while (tile_element_iterator_next(&it))
|
||
{
|
||
if (it.element->GetType() != TILE_ELEMENT_TYPE_TRACK)
|
||
continue;
|
||
if (it.element->AsTrack()->GetRideIndex() != ride->id)
|
||
continue;
|
||
if (it.element->IsGhost())
|
||
continue;
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006847BA
|
||
*/
|
||
void set_vehicle_type_image_max_sizes(rct_ride_entry_vehicle* vehicle_type, int32_t num_images)
|
||
{
|
||
uint8_t bitmap[200][200] = { 0 };
|
||
|
||
rct_drawpixelinfo dpi = {
|
||
/*.bits = */ reinterpret_cast<uint8_t*>(bitmap),
|
||
/*.x = */ -100,
|
||
/*.y = */ -100,
|
||
/*.width = */ 200,
|
||
/*.height = */ 200,
|
||
/*.pitch = */ 0,
|
||
/*.zoom_level = */ 0,
|
||
};
|
||
|
||
for (int32_t i = 0; i < num_images; ++i)
|
||
{
|
||
gfx_draw_sprite_software(&dpi, ImageId::FromUInt32(vehicle_type->base_image_id + i), { 0, 0 });
|
||
}
|
||
int32_t al = -1;
|
||
for (int32_t i = 99; i != 0; --i)
|
||
{
|
||
for (int32_t j = 0; j < 200; j++)
|
||
{
|
||
if (bitmap[j][100 - i] != 0)
|
||
{
|
||
al = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (al != -1)
|
||
break;
|
||
|
||
for (int32_t j = 0; j < 200; j++)
|
||
{
|
||
if (bitmap[j][100 + i] != 0)
|
||
{
|
||
al = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (al != -1)
|
||
break;
|
||
}
|
||
|
||
al++;
|
||
int32_t bl = -1;
|
||
|
||
for (int32_t i = 99; i != 0; --i)
|
||
{
|
||
for (int32_t j = 0; j < 200; j++)
|
||
{
|
||
if (bitmap[100 - i][j] != 0)
|
||
{
|
||
bl = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (bl != -1)
|
||
break;
|
||
}
|
||
bl++;
|
||
|
||
int32_t bh = -1;
|
||
|
||
for (int32_t i = 99; i != 0; --i)
|
||
{
|
||
for (int32_t j = 0; j < 200; j++)
|
||
{
|
||
if (bitmap[100 + i][j] != 0)
|
||
{
|
||
bh = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (bh != -1)
|
||
break;
|
||
}
|
||
bh++;
|
||
|
||
// Moved from object paint
|
||
|
||
if (vehicle_type->flags & VEHICLE_ENTRY_FLAG_SPRITE_BOUNDS_INCLUDE_INVERTED_SET)
|
||
{
|
||
bl += 16;
|
||
}
|
||
|
||
vehicle_type->sprite_width = al;
|
||
vehicle_type->sprite_height_negative = bl;
|
||
vehicle_type->sprite_height_positive = bh;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B59C6
|
||
*/
|
||
void invalidate_test_results(Ride* ride)
|
||
{
|
||
ride->measurement = {};
|
||
ride->excitement = RIDE_RATING_UNDEFINED;
|
||
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->num_vehicles; i++)
|
||
{
|
||
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[i]);
|
||
if (vehicle != nullptr)
|
||
{
|
||
vehicle->ClearUpdateFlag(VEHICLE_UPDATE_FLAG_TESTING);
|
||
}
|
||
}
|
||
}
|
||
window_invalidate_by_number(WC_RIDE, static_cast<uint32_t>(ride->id));
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006B7481
|
||
*
|
||
* @param rideIndex (dl)
|
||
* @param reliabilityIncreaseFactor (ax)
|
||
*/
|
||
void ride_fix_breakdown(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->num_vehicles; i++)
|
||
{
|
||
for (Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[i]); vehicle != nullptr;
|
||
vehicle = GetEntity<Vehicle>(vehicle->next_vehicle_on_train))
|
||
{
|
||
vehicle->ClearUpdateFlag(VEHICLE_UPDATE_FLAG_ZERO_VELOCITY);
|
||
vehicle->ClearUpdateFlag(VEHICLE_UPDATE_FLAG_BROKEN_CAR);
|
||
vehicle->ClearUpdateFlag(VEHICLE_UPDATE_FLAG_BROKEN_TRAIN);
|
||
}
|
||
}
|
||
}
|
||
|
||
uint8_t unreliability = 100 - ride->reliability_percentage;
|
||
ride->reliability += reliabilityIncreaseFactor * (unreliability / 2);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DE102
|
||
*/
|
||
void ride_update_vehicle_colours(Ride* ride)
|
||
{
|
||
if (ride->type == RIDE_TYPE_SPACE_RINGS || ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_VEHICLE_IS_INTEGRAL))
|
||
{
|
||
gfx_invalidate_screen();
|
||
}
|
||
|
||
for (int32_t i = 0; i <= MAX_VEHICLES_PER_RIDE; 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_ALL_SAME:
|
||
colours = ride->vehicle_colours[0];
|
||
colours.Ternary = ride->vehicle_colours[0].Ternary;
|
||
break;
|
||
case RIDE_COLOUR_SCHEME_DIFFERENT_PER_TRAIN:
|
||
colours = ride->vehicle_colours[i];
|
||
colours.Ternary = ride->vehicle_colours[i].Ternary;
|
||
break;
|
||
case RIDE_COLOUR_SCHEME_DIFFERENT_PER_CAR:
|
||
colours = ride->vehicle_colours[std::min(carIndex, MAX_CARS_PER_TRAIN - 1)];
|
||
colours.Ternary = ride->vehicle_colours[std::min(carIndex, MAX_CARS_PER_TRAIN - 1)].Ternary;
|
||
break;
|
||
}
|
||
|
||
vehicle->colours.body_colour = colours.Body;
|
||
vehicle->colours.trim_colour = colours.Trim;
|
||
vehicle->colours_extended = colours.Ternary;
|
||
vehicle->Invalidate();
|
||
carIndex++;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DE4CD
|
||
* trainLayout: Originally fixed to 0x00F64E38. This no longer postfixes with 255.
|
||
*/
|
||
void ride_entry_get_train_layout(int32_t rideEntryIndex, int32_t numCarsPerTrain, uint8_t* trainLayout)
|
||
{
|
||
for (int32_t i = 0; i < numCarsPerTrain; i++)
|
||
{
|
||
trainLayout[i] = ride_entry_get_vehicle_at_position(rideEntryIndex, numCarsPerTrain, i);
|
||
}
|
||
}
|
||
|
||
uint8_t ride_entry_get_vehicle_at_position(int32_t rideEntryIndex, int32_t numCarsPerTrain, int32_t position)
|
||
{
|
||
rct_ride_entry* rideEntry = get_ride_entry(rideEntryIndex);
|
||
if (position == 0 && rideEntry->front_vehicle != 255)
|
||
{
|
||
return rideEntry->front_vehicle;
|
||
}
|
||
if (position == 1 && rideEntry->second_vehicle != 255)
|
||
{
|
||
return rideEntry->second_vehicle;
|
||
}
|
||
if (position == 2 && rideEntry->third_vehicle != 255)
|
||
{
|
||
return rideEntry->third_vehicle;
|
||
}
|
||
if (position == numCarsPerTrain - 1 && rideEntry->rear_vehicle != 255)
|
||
{
|
||
return rideEntry->rear_vehicle;
|
||
}
|
||
|
||
return rideEntry->default_vehicle;
|
||
}
|
||
|
||
// Finds track pieces that a given ride entry has sprites for
|
||
uint64_t ride_entry_get_supported_track_pieces(const rct_ride_entry* rideEntry)
|
||
{
|
||
// clang-format off
|
||
static constexpr uint32_t trackPieceRequiredSprites[TRACK_GROUP_COUNT] =
|
||
{
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_FLAT
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_STRAIGHT
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_STATION_END
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_LIFT_HILL
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES, // TRACK_LIFT_HILL_STEEP
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_LIFT_HILL_CURVE
|
||
VEHICLE_SPRITE_FLAG_FLAT_BANKED, // TRACK_FLAT_ROLL_BANKING
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES | VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_VERTICAL_LOOP
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_SLOPE
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES, // TRACK_SLOPE_STEEP
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES, // TRACK_SLOPE_LONG
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_SLOPE_CURVE
|
||
VEHICLE_SPRITE_FLAG_STEEP_SLOPES, // TRACK_SLOPE_CURVE_STEEP
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_S_BEND
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_CURVE_VERY_SMALL
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_CURVE_SMALL
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_CURVE
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_BANKED | VEHICLE_SPRITE_FLAG_INLINE_TWISTS, // TRACK_TWIST
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES | VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_HALF_LOOP
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_TO_GENTLE_SLOPE_BANKED_TRANSITIONS | VEHICLE_SPRITE_FLAG_CORKSCREWS, // TRACK_CORKSCREW
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_TOWER_BASE
|
||
VEHICLE_SPRITE_FLAG_FLAT_BANKED, // TRACK_HELIX_SMALL
|
||
VEHICLE_SPRITE_FLAG_FLAT_BANKED, // TRACK_HELIX_LARGE
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_HELIX_LARGE_UNBANKED
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_BRAKES
|
||
0, // TRACK_25
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_ON_RIDE_PHOTO
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_WATER_SPLASH
|
||
VEHICLE_SPRITE_FLAG_STEEP_SLOPES | VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_SLOPE_VERTICAL
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_BANKED | VEHICLE_SPRITE_FLAG_INLINE_TWISTS, // TRACK_BARREL_ROLL
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_POWERED_LIFT
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES | VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_HALF_LOOP_LARGE
|
||
VEHICLE_SPRITE_FLAG_FLAT_TO_GENTLE_SLOPE_BANKED_TRANSITIONS | VEHICLE_SPRITE_FLAG_GENTLE_SLOPE_BANKED_TURNS, // TRACK_SLOPE_CURVE_BANKED
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_LOG_FLUME_REVERSER
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_BANKED | VEHICLE_SPRITE_FLAG_INLINE_TWISTS, // TRACK_HEARTLINE_ROLL
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_REVERSER
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_REVERSE_FREEFALL
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES | VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_SLOPE_TO_FLAT
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_BLOCK_BRAKES
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPE_BANKED_TRANSITIONS, // TRACK_SLOPE_ROLL_BANKING
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES, // TRACK_SLOPE_STEEP_LONG
|
||
VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_CURVE_VERTICAL
|
||
0, // TRACK_42
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_LIFT_HILL_CABLE
|
||
VEHICLE_SPRITE_FLAG_CURVED_LIFT_HILL, // TRACK_LIFT_HILL_CURVED
|
||
VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_QUARTER_LOOP
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_SPINNING_TUNNEL
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_ROTATION_CONTROL_TOGGLE
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_BANKED | VEHICLE_SPRITE_FLAG_INLINE_TWISTS, // TRACK_INLINE_TWIST_UNINVERTED
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_BANKED | VEHICLE_SPRITE_FLAG_INLINE_TWISTS, // TRACK_INLINE_TWIST_INVERTED
|
||
VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_QUARTER_LOOP_UNINVERTED
|
||
VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_QUARTER_LOOP_INVERTED
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_RAPIDS
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES | VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_HALF_LOOP_UNINVERTED
|
||
VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES | VEHICLE_SPRITE_FLAG_VERTICAL_SLOPES, // TRACK_HALF_LOOP_INVERTED
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_WATERFALL
|
||
VEHICLE_SPRITE_FLAG_FLAT, // TRACK_WHIRLPOOL
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_GENTLE_SLOPES | VEHICLE_SPRITE_FLAG_STEEP_SLOPES, // TRACK_BRAKE_FOR_DROP
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_TO_GENTLE_SLOPE_BANKED_TRANSITIONS | VEHICLE_SPRITE_FLAG_CORKSCREWS, // TRACK_CORKSCREW_UNINVERTED
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_FLAT_TO_GENTLE_SLOPE_BANKED_TRANSITIONS | VEHICLE_SPRITE_FLAG_CORKSCREWS, // TRACK_CORKSCREW_INVERTED
|
||
VEHICLE_SPRITE_FLAG_FLAT | VEHICLE_SPRITE_FLAG_GENTLE_SLOPES, // TRACK_HEARTLINE_TRANSFER
|
||
0, // TRACK_MINI_GOLF_HOLE
|
||
};
|
||
// clang-format on
|
||
|
||
// 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 = std::numeric_limits<uint64_t>::max();
|
||
auto defaultVehicle = rideEntry->GetDefaultVehicle();
|
||
if (defaultVehicle != nullptr)
|
||
{
|
||
const auto defaultSpriteFlags = defaultVehicle->sprite_flags;
|
||
for (size_t i = 0; i < std::size(trackPieceRequiredSprites); i++)
|
||
{
|
||
if ((defaultSpriteFlags & trackPieceRequiredSprites[i]) != trackPieceRequiredSprites[i])
|
||
{
|
||
supportedPieces &= ~(1ULL << i);
|
||
}
|
||
}
|
||
}
|
||
return supportedPieces;
|
||
}
|
||
|
||
static std::optional<int32_t> ride_get_smallest_station_length(Ride* ride)
|
||
{
|
||
std::optional<int32_t> result;
|
||
for (const auto& station : ride->stations)
|
||
{
|
||
if (!station.Start.IsNull())
|
||
{
|
||
if (!result.has_value() || station.Length < result.value())
|
||
{
|
||
result = station.Length;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006CB3AA
|
||
*/
|
||
static int32_t ride_get_track_length(Ride* ride)
|
||
{
|
||
TileElement* tileElement = nullptr;
|
||
track_type_t trackType;
|
||
CoordsXYZ trackStart;
|
||
bool foundTrack = false;
|
||
|
||
for (int32_t i = 0; i < MAX_STATIONS && !foundTrack; i++)
|
||
{
|
||
trackStart = ride->stations[i].GetStart();
|
||
if (trackStart.IsNull())
|
||
continue;
|
||
|
||
tileElement = map_get_first_element_at(trackStart);
|
||
if (tileElement == nullptr)
|
||
continue;
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
|
||
continue;
|
||
|
||
trackType = tileElement->AsTrack()->GetTrackType();
|
||
const auto& ted = GetTrackElementDescriptor(trackType);
|
||
if (!(ted.SequenceProperties[0] & TRACK_SEQUENCE_FLAG_ORIGIN))
|
||
continue;
|
||
|
||
if (tileElement->GetBaseZ() != trackStart.z)
|
||
continue;
|
||
|
||
foundTrack = true;
|
||
} while (!foundTrack && !(tileElement++)->IsLastForTile());
|
||
}
|
||
|
||
if (!foundTrack)
|
||
return 0;
|
||
|
||
ride_id_t rideIndex = tileElement->AsTrack()->GetRideIndex();
|
||
|
||
rct_window* w = window_find_by_class(WC_RIDE_CONSTRUCTION);
|
||
if (w != nullptr && _rideConstructionState != RideConstructionState::State0 && _currentRideIndex == rideIndex)
|
||
{
|
||
ride_construction_invalidate_current_track();
|
||
}
|
||
|
||
bool moveSlowIt = true;
|
||
int32_t result = 0;
|
||
|
||
track_circuit_iterator it;
|
||
track_circuit_iterator_begin(&it, { trackStart.x, trackStart.y, tileElement });
|
||
|
||
track_circuit_iterator slowIt = it;
|
||
while (track_circuit_iterator_next(&it))
|
||
{
|
||
trackType = it.current.element->AsTrack()->GetTrackType();
|
||
const auto& ted = GetTrackElementDescriptor(trackType);
|
||
result += ted.PieceLength;
|
||
|
||
moveSlowIt = !moveSlowIt;
|
||
if (moveSlowIt)
|
||
{
|
||
track_circuit_iterator_next(&slowIt);
|
||
if (track_circuit_iterators_match(&it, &slowIt))
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* rct2: 0x006DD57D
|
||
*/
|
||
void Ride::UpdateMaxVehicles()
|
||
{
|
||
if (subtype == OBJECT_ENTRY_INDEX_NULL)
|
||
return;
|
||
|
||
rct_ride_entry* rideEntry = get_ride_entry(subtype);
|
||
if (rideEntry == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
rct_ride_entry_vehicle* vehicleEntry;
|
||
uint8_t numCarsPerTrain, numVehicles;
|
||
int32_t maxNumTrains;
|
||
|
||
if (rideEntry->cars_per_flat_ride == 0xFF)
|
||
{
|
||
int32_t trainLength;
|
||
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 = ride_get_smallest_station_length(this);
|
||
if (!stationNumTiles.has_value())
|
||
return;
|
||
|
||
auto stationLength = (stationNumTiles.value() * 0x44180) - 0x16B2A;
|
||
int32_t maxMass = GetRideTypeDescriptor().MaxMass << 8;
|
||
int32_t maxCarsPerTrain = 1;
|
||
for (int32_t numCars = rideEntry->max_cars_in_train; numCars > 0; numCars--)
|
||
{
|
||
trainLength = 0;
|
||
int32_t totalMass = 0;
|
||
for (int32_t i = 0; i < numCars; i++)
|
||
{
|
||
vehicleEntry = &rideEntry->vehicles[ride_entry_get_vehicle_at_position(subtype, numCars, i)];
|
||
trainLength += vehicleEntry->spacing;
|
||
totalMass += vehicleEntry->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 (!gCheatsDisableTrainLengthLimit)
|
||
{
|
||
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, MAX_VEHICLES_PER_RIDE);
|
||
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
|
||
trainLength = 0;
|
||
for (int32_t i = 0; i < newCarsPerTrain; i++)
|
||
{
|
||
vehicleEntry = &rideEntry->vehicles[ride_entry_get_vehicle_at_position(subtype, newCarsPerTrain, i)];
|
||
trainLength += vehicleEntry->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)
|
||
|| !(GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_ALLOW_MORE_VEHICLES_THAN_STATION_FITS)))
|
||
{
|
||
maxNumTrains = std::min(maxNumTrains, int32_t(MAX_VEHICLES_PER_RIDE));
|
||
}
|
||
else
|
||
{
|
||
vehicleEntry = &rideEntry->vehicles[ride_entry_get_vehicle_at_position(subtype, newCarsPerTrain, 0)];
|
||
int32_t poweredMaxSpeed = vehicleEntry->powered_max_speed;
|
||
|
||
int32_t totalSpacing = 0;
|
||
for (int32_t i = 0; i < newCarsPerTrain; i++)
|
||
{
|
||
vehicleEntry = &rideEntry->vehicles[ride_entry_get_vehicle_at_position(subtype, newCarsPerTrain, i)];
|
||
totalSpacing += vehicleEntry->spacing;
|
||
}
|
||
|
||
totalSpacing >>= 13;
|
||
int32_t trackLength = ride_get_track_length(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 < MAX_VEHICLES_PER_RIDE && 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 (gCheatsDisableTrainLengthLimit)
|
||
{
|
||
maxNumTrains = MAX_VEHICLES_PER_RIDE;
|
||
}
|
||
numVehicles = std::min(proposed_num_vehicles, static_cast<uint8_t>(maxNumTrains));
|
||
|
||
// Refresh new current num vehicles / num cars per vehicle
|
||
if (numVehicles != num_vehicles || numCarsPerTrain != num_cars_per_train)
|
||
{
|
||
num_cars_per_train = numCarsPerTrain;
|
||
num_vehicles = numVehicles;
|
||
window_invalidate_by_number(WC_RIDE, EnumValue(id));
|
||
}
|
||
}
|
||
|
||
void Ride::UpdateNumberOfCircuits()
|
||
{
|
||
if (!CanHaveMultipleCircuits())
|
||
{
|
||
num_circuits = 1;
|
||
}
|
||
}
|
||
|
||
void Ride::SetRideEntry(int32_t rideEntry)
|
||
{
|
||
auto colour = ride_get_unused_preset_vehicle_colour(rideEntry);
|
||
auto rideSetVehicleAction = RideSetVehicleAction(id, RideSetVehicleType::RideEntry, rideEntry, colour);
|
||
GameActions::Execute(&rideSetVehicleAction);
|
||
}
|
||
|
||
void Ride::SetNumVehicles(int32_t numVehicles)
|
||
{
|
||
auto rideSetVehicleAction = RideSetVehicleAction(id, RideSetVehicleType::NumTrains, numVehicles);
|
||
GameActions::Execute(&rideSetVehicleAction);
|
||
}
|
||
|
||
void Ride::SetNumCarsPerVehicle(int32_t numCarsPerVehicle)
|
||
{
|
||
auto rideSetVehicleAction = RideSetVehicleAction(id, RideSetVehicleType::NumCarsPerTrain, numCarsPerVehicle);
|
||
GameActions::Execute(&rideSetVehicleAction);
|
||
}
|
||
|
||
void Ride::SetToDefaultInspectionInterval()
|
||
{
|
||
uint8_t defaultInspectionInterval = gConfigGeneral.default_inspection_interval;
|
||
if (inspection_interval != defaultInspectionInterval)
|
||
{
|
||
if (defaultInspectionInterval <= RIDE_INSPECTION_NEVER)
|
||
{
|
||
set_operating_setting(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);
|
||
rct_window* w = context_open_intent(&intent);
|
||
|
||
rct_viewport* viewport = window_get_viewport(w);
|
||
if (w != nullptr && viewport != nullptr)
|
||
{
|
||
viewport->flags |= VIEWPORT_FLAG_SOUND_ON;
|
||
}
|
||
}
|
||
|
||
if (gConfigNotifications.ride_crashed)
|
||
{
|
||
Formatter ft;
|
||
FormatNameTo(ft);
|
||
News::AddItemToQueue(News::ItemType::Ride, STR_RIDE_HAS_CRASHED, EnumValue(id), ft);
|
||
}
|
||
}
|
||
|
||
void ride_reset_all_names()
|
||
{
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
ride.SetNameToDefault();
|
||
}
|
||
}
|
||
|
||
// Gets the approximate value of customers per hour for this ride. Multiplies ride_customers_in_last_5_minutes() by 12.
|
||
uint32_t ride_customers_per_hour(const Ride* ride)
|
||
{
|
||
return ride_customers_in_last_5_minutes(ride) * 12;
|
||
}
|
||
|
||
// Calculates the number of customers for this ride in the last 5 minutes (or more correctly 9600 game ticks)
|
||
uint32_t ride_customers_in_last_5_minutes(const Ride* ride)
|
||
{
|
||
uint32_t sum = 0;
|
||
|
||
for (int32_t i = 0; i < CUSTOMER_HISTORY_SIZE; i++)
|
||
{
|
||
sum += ride->num_customers[i];
|
||
}
|
||
|
||
return sum;
|
||
}
|
||
|
||
Vehicle* ride_get_broken_vehicle(const Ride* ride)
|
||
{
|
||
uint16_t 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()
|
||
{
|
||
custom_name = {};
|
||
measurement = {};
|
||
type = RIDE_TYPE_NULL;
|
||
}
|
||
|
||
void Ride::Renew()
|
||
{
|
||
// Set build date to current date (so the ride is brand new)
|
||
build_date = gDateMonthsElapsed;
|
||
reliability = RIDE_INITIAL_RELIABILITY;
|
||
}
|
||
|
||
RideClassification Ride::GetClassification() const
|
||
{
|
||
switch (type)
|
||
{
|
||
case RIDE_TYPE_FOOD_STALL:
|
||
case RIDE_TYPE_1D:
|
||
case RIDE_TYPE_DRINK_STALL:
|
||
case RIDE_TYPE_1F:
|
||
case RIDE_TYPE_SHOP:
|
||
case RIDE_TYPE_22:
|
||
case RIDE_TYPE_50:
|
||
case RIDE_TYPE_52:
|
||
case RIDE_TYPE_53:
|
||
case RIDE_TYPE_54:
|
||
return RideClassification::ShopOrStall;
|
||
case RIDE_TYPE_INFORMATION_KIOSK:
|
||
case RIDE_TYPE_TOILETS:
|
||
case RIDE_TYPE_CASH_MACHINE:
|
||
case RIDE_TYPE_FIRST_AID:
|
||
return RideClassification::KioskOrFacility;
|
||
default:
|
||
return RideClassification::Ride;
|
||
}
|
||
}
|
||
|
||
bool Ride::IsRide() const
|
||
{
|
||
return GetClassification() == RideClassification::Ride;
|
||
}
|
||
|
||
money16 ride_get_price(const Ride* ride)
|
||
{
|
||
if (gParkFlags & PARK_FLAGS_NO_MONEY)
|
||
return 0;
|
||
if (ride->IsRide())
|
||
{
|
||
if (!park_ride_prices_unlocked())
|
||
{
|
||
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* get_station_platform(const CoordsXYRangedZ& coords)
|
||
{
|
||
bool foundTileElement = false;
|
||
TileElement* tileElement = map_get_first_element_at(coords);
|
||
if (tileElement != nullptr)
|
||
{
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_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 check_for_adjacent_station(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 <= RIDE_ADJACENCY_CHECK_DISTANCE; i++)
|
||
{
|
||
adjX += CoordsDirectionDelta[direction].x;
|
||
adjY += CoordsDirectionDelta[direction].y;
|
||
TileElement* stationElement = get_station_platform(
|
||
{ { adjX, adjY, stationCoords.z }, stationCoords.z + 2 * COORDS_Z_STEP });
|
||
if (stationElement != nullptr)
|
||
{
|
||
auto rideIndex = stationElement->AsTrack()->GetRideIndex();
|
||
auto ride = get_ride(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 ride_has_adjacent_station(Ride* ride)
|
||
{
|
||
bool found = false;
|
||
|
||
/* Loop through all of the ride stations, checking for an
|
||
* adjacent station on either side. */
|
||
for (StationIndex stationNum = 0; stationNum < MAX_STATIONS; stationNum++)
|
||
{
|
||
auto stationStart = ride->stations[stationNum].GetStart();
|
||
if (!stationStart.IsNull())
|
||
{
|
||
/* Get the map element for the station start. */
|
||
TileElement* stationElement = get_station_platform({ stationStart, stationStart.z + 0 });
|
||
if (stationElement == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
/* Check the first side of the station */
|
||
int32_t direction = stationElement->GetDirectionWithOffset(1);
|
||
found = check_for_adjacent_station(stationStart, direction);
|
||
if (found)
|
||
break;
|
||
/* Check the other side of the station */
|
||
direction = direction_reverse(direction);
|
||
found = check_for_adjacent_station(stationStart, direction);
|
||
if (found)
|
||
break;
|
||
}
|
||
}
|
||
return found;
|
||
}
|
||
|
||
bool ride_has_station_shelter(Ride* ride)
|
||
{
|
||
auto stationObj = ride_get_station_object(ride);
|
||
return stationObj != nullptr && (stationObj->Flags & STATION_OBJECT_FLAGS::HAS_SHELTER);
|
||
}
|
||
|
||
bool ride_has_ratings(const Ride* ride)
|
||
{
|
||
return ride->excitement != RIDE_RATING_UNDEFINED;
|
||
}
|
||
|
||
/**
|
||
* Searches for a non-null ride type in a ride entry.
|
||
* If none is found, it will still return RIDE_TYPE_NULL.
|
||
*/
|
||
uint8_t ride_entry_get_first_non_null_ride_type(const rct_ride_entry* rideEntry)
|
||
{
|
||
for (uint8_t i = 0; i < MAX_RIDE_TYPES_PER_RIDE_ENTRY; i++)
|
||
{
|
||
if (rideEntry->ride_type[i] != RIDE_TYPE_NULL)
|
||
{
|
||
return rideEntry->ride_type[i];
|
||
}
|
||
}
|
||
return RIDE_TYPE_NULL;
|
||
}
|
||
|
||
int32_t get_booster_speed(uint8_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 fix_invalid_vehicle_sprite_sizes()
|
||
{
|
||
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 vehicleEntry = vehicle->Entry();
|
||
if (vehicleEntry == nullptr)
|
||
{
|
||
break;
|
||
}
|
||
|
||
if (vehicle->sprite_width == 0)
|
||
{
|
||
vehicle->sprite_width = vehicleEntry->sprite_width;
|
||
}
|
||
if (vehicle->sprite_height_negative == 0)
|
||
{
|
||
vehicle->sprite_height_negative = vehicleEntry->sprite_height_negative;
|
||
}
|
||
if (vehicle->sprite_height_positive == 0)
|
||
{
|
||
vehicle->sprite_height_positive = vehicleEntry->sprite_height_positive;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
bool ride_entry_has_category(const rct_ride_entry* rideEntry, uint8_t category)
|
||
{
|
||
auto rideType = ride_entry_get_first_non_null_ride_type(rideEntry);
|
||
return GetRideTypeDescriptor(rideType).Category == category;
|
||
}
|
||
|
||
int32_t ride_get_entry_index(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)
|
||
{
|
||
auto rideEntry = get_ride_entry(rideEntryIndex);
|
||
if (rideEntry == nullptr)
|
||
{
|
||
return OBJECT_ENTRY_INDEX_NULL;
|
||
}
|
||
|
||
// Can happen in select-by-track-type mode
|
||
if (!ride_entry_is_invented(rideEntryIndex) && !gCheatsIgnoreResearchStatus)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!GetRideTypeDescriptor(rideType).HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
|
||
{
|
||
subType = rideEntryIndex;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return subType;
|
||
}
|
||
|
||
StationObject* ride_get_station_object(const Ride* ride)
|
||
{
|
||
auto& objManager = GetContext()->GetObjectManager();
|
||
return static_cast<StationObject*>(objManager.GetLoadedObject(ObjectType::Station, ride->entrance_style));
|
||
}
|
||
|
||
// 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 determine_ride_entrance_and_exit_locations()
|
||
{
|
||
log_verbose("Inspecting ride entrance / exit locations");
|
||
|
||
for (auto& ride : GetRideManager())
|
||
{
|
||
for (StationIndex stationIndex = 0; stationIndex < MAX_STATIONS; stationIndex++)
|
||
{
|
||
TileCoordsXYZD entranceLoc = ride.stations[stationIndex].Entrance;
|
||
TileCoordsXYZD exitLoc = ride.stations[stationIndex].Exit;
|
||
bool fixEntrance = false;
|
||
bool fixExit = false;
|
||
|
||
// Skip if the station has no entrance
|
||
if (!entranceLoc.IsNull())
|
||
{
|
||
const EntranceElement* entranceElement = map_get_ride_entrance_element_at(entranceLoc.ToCoordsXYZD(), false);
|
||
|
||
if (entranceElement == nullptr || entranceElement->GetRideIndex() != ride.id
|
||
|| entranceElement->GetStationIndex() != stationIndex)
|
||
{
|
||
fixEntrance = true;
|
||
}
|
||
else
|
||
{
|
||
ride.stations[stationIndex].Entrance.direction = static_cast<uint8_t>(entranceElement->GetDirection());
|
||
}
|
||
}
|
||
|
||
if (!exitLoc.IsNull())
|
||
{
|
||
const EntranceElement* entranceElement = map_get_ride_exit_element_at(exitLoc.ToCoordsXYZD(), false);
|
||
|
||
if (entranceElement == nullptr || entranceElement->GetRideIndex() != ride.id
|
||
|| entranceElement->GetStationIndex() != stationIndex)
|
||
{
|
||
fixExit = true;
|
||
}
|
||
else
|
||
{
|
||
ride.stations[stationIndex].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;
|
||
for (int32_t x = 1; x < MAXIMUM_MAP_SIZE_TECHNICAL - 1; x++)
|
||
{
|
||
for (int32_t y = 1; y < MAXIMUM_MAP_SIZE_TECHNICAL - 1; y++)
|
||
{
|
||
TileElement* tileElement = map_get_first_element_at(TileCoordsXY{ x, y });
|
||
|
||
if (tileElement != nullptr)
|
||
{
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_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 = ride.stations[stationIndex].Height;
|
||
|
||
if (fixEntrance && entranceElement->GetEntranceType() == ENTRANCE_TYPE_RIDE_ENTRANCE)
|
||
{
|
||
if (alreadyFoundEntrance)
|
||
{
|
||
if (ride.stations[stationIndex].Entrance.z == expectedHeight)
|
||
continue;
|
||
if (ride.stations[stationIndex].Entrance.z > entranceElement->base_height)
|
||
continue;
|
||
}
|
||
|
||
// Found our entrance
|
||
TileCoordsXYZD newEntranceLoc = {
|
||
x,
|
||
y,
|
||
entranceElement->base_height,
|
||
static_cast<uint8_t>(entranceElement->GetDirection()),
|
||
};
|
||
ride_set_entrance_location(&ride, stationIndex, newEntranceLoc);
|
||
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->base_height);
|
||
}
|
||
else if (fixExit && entranceElement->GetEntranceType() == ENTRANCE_TYPE_RIDE_EXIT)
|
||
{
|
||
if (alreadyFoundExit)
|
||
{
|
||
if (ride.stations[stationIndex].Exit.z == expectedHeight)
|
||
continue;
|
||
if (ride.stations[stationIndex].Exit.z > entranceElement->base_height)
|
||
continue;
|
||
}
|
||
|
||
// Found our exit
|
||
ride_set_exit_location(
|
||
&ride, stationIndex,
|
||
{ x, y, entranceElement->base_height,
|
||
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->base_height);
|
||
}
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
}
|
||
|
||
if (fixEntrance && !alreadyFoundEntrance)
|
||
{
|
||
ride_clear_entrance_location(&ride, stationIndex);
|
||
log_verbose("Cleared disconnected entrance of ride %d, station %d.", ride.id, stationIndex);
|
||
}
|
||
if (fixExit && !alreadyFoundExit)
|
||
{
|
||
ride_clear_exit_location(&ride, stationIndex);
|
||
log_verbose("Cleared disconnected exit of ride %d, station %d.", ride.id, stationIndex);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void ride_clear_leftover_entrances(Ride* ride)
|
||
{
|
||
tile_element_iterator it;
|
||
|
||
tile_element_iterator_begin(&it);
|
||
while (tile_element_iterator_next(&it))
|
||
{
|
||
if (it.element->GetType() == TILE_ELEMENT_TYPE_ENTRANCE
|
||
&& it.element->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_PARK_ENTRANCE
|
||
&& it.element->AsEntrance()->GetRideIndex() == ride->id)
|
||
{
|
||
tile_element_remove(it.element);
|
||
tile_element_iterator_restart_for_tile(&it);
|
||
}
|
||
}
|
||
}
|
||
|
||
std::string Ride::GetName() const
|
||
{
|
||
Formatter ft;
|
||
FormatNameTo(ft);
|
||
return format_string(STR_STRINGID, ft.Data());
|
||
}
|
||
|
||
void Ride::FormatNameTo(Formatter& ft) const
|
||
{
|
||
if (!custom_name.empty())
|
||
{
|
||
auto str = custom_name.c_str();
|
||
ft.Add<rct_string_id>(STR_STRING);
|
||
ft.Add<const char*>(str);
|
||
}
|
||
else
|
||
{
|
||
auto rideTypeName = GetRideTypeDescriptor().Naming.Name;
|
||
if (GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_LIST_VEHICLES_SEPARATELY))
|
||
{
|
||
auto rideEntry = GetRideEntry();
|
||
if (rideEntry != nullptr)
|
||
{
|
||
rideTypeName = rideEntry->naming.Name;
|
||
}
|
||
}
|
||
ft.Add<rct_string_id>(1).Add<rct_string_id>(rideTypeName).Add<uint16_t>(default_name_number);
|
||
}
|
||
}
|
||
|
||
uint64_t Ride::GetAvailableModes() const
|
||
{
|
||
if (gCheatsShowAllOperatingModes)
|
||
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()
|
||
{
|
||
for (int32_t y = 0; y < MAXIMUM_MAP_SIZE_TECHNICAL; y++)
|
||
{
|
||
for (int32_t x = 0; x < MAXIMUM_MAP_SIZE_TECHNICAL; x++)
|
||
{
|
||
auto* tileElement = map_get_first_element_at(TileCoordsXY(x, y));
|
||
if (tileElement == nullptr)
|
||
continue;
|
||
|
||
do
|
||
{
|
||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
|
||
continue;
|
||
|
||
auto* trackElement = tileElement->AsTrack();
|
||
if (trackElement->GetRideIndex() != id)
|
||
continue;
|
||
|
||
trackElement->SetRideType(type);
|
||
|
||
} while (!(tileElement++)->IsLastForTile());
|
||
}
|
||
}
|
||
}
|
||
|
||
std::vector<ride_id_t> GetTracklessRides()
|
||
{
|
||
// Iterate map and build list of seen ride IDs
|
||
std::vector<bool> seen;
|
||
seen.resize(256);
|
||
tile_element_iterator it;
|
||
tile_element_iterator_begin(&it);
|
||
while (tile_element_iterator_next(&it))
|
||
{
|
||
auto trackEl = it.element->AsTrack();
|
||
if (trackEl != nullptr && !trackEl->IsGhost())
|
||
{
|
||
auto rideId = static_cast<size_t>(trackEl->GetRideIndex());
|
||
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<ride_id_t> result;
|
||
for (const auto& ride : rideManager)
|
||
{
|
||
if (seen.size() <= static_cast<size_t>(ride.id) || !seen[static_cast<size_t>(ride.id)])
|
||
{
|
||
result.push_back(ride.id);
|
||
}
|
||
}
|
||
return result;
|
||
}
|