/***************************************************************************** * 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 #include #include #include #include #include #include using namespace OpenRCT2; using namespace OpenRCT2::TrackMetaData; RideMode& operator++(RideMode& d, int) { return d = (d == RideMode::Count) ? RideMode::Normal : static_cast(static_cast(d) + 1); } static constexpr const int32_t RideInspectionInterval[] = { 10, 20, 30, 45, 60, 120, 0, 0, }; static std::vector _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(result); } Ride* GetOrAllocateRide(ride_id_t index) { const auto idx = static_cast(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(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(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(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(queueTime); } Guest* Ride::GetQueueHeadGuest(StationIndex stationIndex) const { Guest* peep; Guest* result = nullptr; uint16_t spriteIndex = stations[stationIndex].LastPeepInQueue; while ((peep = TryGetEntity(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(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()) { 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(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(STR_CRASHED); } else if (lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN) { ft.Add(STR_BROKEN_DOWN); } else if (status == RideStatus::Closed) { if (!GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP)) { if (num_riders != 0) { ft.Add(num_riders == 1 ? STR_CLOSED_WITH_PERSON : STR_CLOSED_WITH_PEOPLE); ft.Add(num_riders); } else { ft.Add(STR_CLOSED); } } else { ft.Add(STR_CLOSED); } } else if (status == RideStatus::Simulating) { ft.Add(STR_SIMULATING); } else if (status == RideStatus::Testing) { ft.Add(STR_TEST_RUN); } else if ( mode == RideMode::Race && !(lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING) && race_winner != SPRITE_INDEX_NULL) { auto peep = GetEntity(race_winner); if (peep != nullptr) { ft.Add(STR_RACE_WON_BY); peep->FormatNameTo(ft); } else { ft.Add(STR_RACE_WON_BY); ft.Add(STR_NONE); } } else if (!GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP)) { ft.Add(num_riders == 1 ? STR_PERSON_ON_RIDE : STR_PEOPLE_ON_RIDE); ft.Add(num_riders); } else { ft.Add(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 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(); 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(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(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(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(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(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(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(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::max(); for (auto peep : EntityList()) { 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(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(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(objManager.GetLoadedObject(ObjectType::Music, ride->music)); if (musicObj != nullptr) { auto numTracks = musicObj->GetTrackCount(); ride->music_tune_id = static_cast(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(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(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 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(); 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(GetRideComponentName(GetRideTypeDescriptor().NameConvention.vehicle).singular); ft.Add(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(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(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(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(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(STR_RIDE_MAP_TIP); ft.Add(ride->num_stations <= 1 ? STR_RIDE_STATION : STR_RIDE_STATION_X); ride->FormatNameTo(ft); ft.Add(GetRideComponentName(ride->GetRideTypeDescriptor().NameConvention.station).capitalised); ft.Add(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(STR_RIDE_MAP_TIP); ft.Add(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(stationIndex + 1); if (queueLength == 0) { ft.Add(STR_QUEUE_EMPTY); } else if (queueLength == 1) { ft.Add(STR_QUEUE_ONE_PERSON); } else { ft.Add(STR_QUEUE_PEOPLE); } ft.Add(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(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(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) }, 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(); 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(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(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(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(trackElement) }; return; } } outXYElement->x = curTrackPos.x; outXYElement->y = curTrackPos.y; outXYElement->element = reinterpret_cast(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(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(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(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(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(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(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()) { 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(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(ride->vehicles[i]); if (vehicle != nullptr) { vehicle->ClearUpdateFlag(VEHICLE_UPDATE_FLAG_TESTING); } } } window_invalidate_by_number(WC_RIDE, static_cast(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(ride->vehicles[i]); vehicle != nullptr; vehicle = GetEntity(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(ride->vehicles[i]); vehicle != nullptr; vehicle = GetEntity(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::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 ride_get_smallest_station_length(Ride* ride) { std::optional 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(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(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(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(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(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(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); if (network_get_mode() != NETWORK_MODE_NONE) { // The server might run in headless mode so no images will be loaded, only check for stations. return stationObj != nullptr; } return stationObj != nullptr && stationObj->BaseImageId != 0; } 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(entityIndex); vehicle != nullptr; vehicle = TryGetEntity(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(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(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(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(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(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(STR_STRING); ft.Add(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(1).Add(rideTypeName).Add(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 GetTracklessRides() { // Iterate map and build list of seen ride IDs std::vector 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(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 result; for (const auto& ride : rideManager) { if (seen.size() <= static_cast(ride.id) || !seen[static_cast(ride.id)]) { result.push_back(ride.id); } } return result; }