/*****************************************************************************
 * Copyright (c) 2014-2026 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 "../Diagnostic.h"
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../actions/GameActionRunner.h"
#include "../actions/ResultWithMessage.h"
#include "../actions/ride/RideSetStatusAction.h"
#include "../actions/ride/RideSetVehicleAction.h"
#include "../audio/Audio.h"
#include "../config/Config.h"
#include "../core/BitSet.hpp"
#include "../core/EnumUtils.hpp"
#include "../core/Guard.hpp"
#include "../core/Numerics.hpp"
#include "../drawing/Drawing.h"
#include "../entity/EntityList.h"
#include "../entity/EntityRegistry.h"
#include "../entity/Peep.h"
#include "../entity/Staff.h"
#include "../interface/Viewport.h"
#include "../interface/WidgetIndexGlobals.h"
#include "../interface/WindowBase.h"
#include "../localisation/Formatter.h"
#include "../localisation/Formatting.h"
#include "../localisation/StringWithArgs.h"
#include "../management/NewsItem.h"
#include "../object/MusicObject.h"
#include "../object/ObjectList.h"
#include "../object/ObjectManager.h"
#include "../object/RideObject.h"
#include "../object/StationObject.h"
#include "../profiling/Profiling.h"
#include "../scenario/Scenario.h"
#include "../scripting/ScriptEngine.h"
#include "../ui/WindowManager.h"
#include "../util/Util.h"
#include "../windows/Intent.h"
#include "../world/Entrance.h"
#include "../world/Footpath.h"
#include "../world/Location.hpp"
#include "../world/Map.h"
#include "../world/Park.h"
#include "../world/TileElementsView.h"
#include "../world/Weather.h"
#include "../world/tile_element/EntranceElement.h"
#include "../world/tile_element/PathElement.h"
#include "../world/tile_element/TrackElement.h"
#include "CableLift.h"
#include "RideAudio.h"
#include "RideBreakdownMap.h"
#include "RideConstruction.h"
#include "RideData.h"
#include "RideEntry.h"
#include "RideManager.hpp"
#include "ShopItem.h"
#include "Station.h"
#include "TrackData.h"
#include "TrackDesign.h"
#include "TrackIteration.h"
#include "Vehicle.h"
#include "ted/TrackElementDescriptor.h"

#include <cassert>
#include <iterator>
#include <limits>
#include <optional>

namespace OpenRCT2
{
    using namespace OpenRCT2::Entity::Yaw;
    using namespace OpenRCT2::TrackMetadata;
    using namespace OpenRCT2::Scripting;

    static constexpr auto kRideModeBlockSectionedCounterpart = std::to_array(
        {
            RideMode::normal,                          // RideMode::normal,
            RideMode::continuousCircuitBlockSectioned, // RideMode::continuousCircuit,
            RideMode::reverseInclineLaunchedShuttle,   // RideMode::reverseInclineLaunchedShuttle,
            RideMode::poweredLaunchBlockSectioned,     // RideMode::poweredLaunchPassthrough,
            RideMode::shuttle,                         // RideMode::shuttle,
            RideMode::boatHire,                        // RideMode::boatHire,
            RideMode::upwardLaunch,                    // RideMode::upwardLaunch,
            RideMode::rotatingLift,                    // RideMode::rotatingLift,
            RideMode::stationToStation,                // RideMode::stationToStation,
            RideMode::singleRidePerAdmission,          // RideMode::singleRidePerAdmission,
            RideMode::unlimitedRidesPerAdmission,      // RideMode::unlimitedRidesPerAdmission ,
            RideMode::maze,                            // RideMode::maze,
            RideMode::race,                            // RideMode::race,
            RideMode::dodgems,                         // RideMode::dodgems,
            RideMode::swing,                           // RideMode::swing,
            RideMode::shopStall,                       // RideMode::shopStall,
            RideMode::rotation,                        // RideMode::rotation,
            RideMode::forwardRotation,                 // RideMode::forwardRotation,
            RideMode::backwardRotation,                // RideMode::backwardRotation,
            RideMode::filmAvengingAviators,            // RideMode::filmAvengingAviators,
            RideMode::mouseTails3DFilm,                // RideMode::mouseTails3DFilm,
            RideMode::spaceRings,                      // RideMode::spaceRings,
            RideMode::beginners,                       // RideMode::beginners,
            RideMode::limPoweredLaunch,                // RideMode::limPoweredLaunch,
            RideMode::filmThrillRiders,                // RideMode::filmThrillRiders,
            RideMode::stormChasers3DFilm,              // RideMode::stormChasers3DFilm,
            RideMode::spaceRaiders3DFilm,              // RideMode::spaceRaiders3DFilm,
            RideMode::intense,                         // RideMode::intense,
            RideMode::berserk,                         // RideMode::berserk,
            RideMode::hauntedHouse,                    // RideMode::hauntedHouse,
            RideMode::circus,                          // RideMode::circus,
            RideMode::downwardLaunch,                  // RideMode::downwardLaunch,
            RideMode::crookedHouse,                    // RideMode::crookedHouse,
            RideMode::freefallDrop,                    // RideMode::freefallDrop,
            RideMode::continuousCircuitBlockSectioned, // RideMode::continuousCircuitBlockSectioned,
            RideMode::poweredLaunchBlockSectioned,     // RideMode::poweredLaunch,
            RideMode::poweredLaunchBlockSectioned,     // RideMode::poweredLaunchBlockSectioned,
        });
    static_assert(kRideModeBlockSectionedCounterpart.size() == EnumValue(RideMode::count));

    RideMode& operator++(RideMode& d, int)
    {
        return d = (d == RideMode::count) ? RideMode::normal : static_cast<RideMode>(static_cast<uint8_t>(d) + 1);
    }

    static constexpr int32_t RideInspectionInterval[] = {
        10, 20, 30, 45, 60, 120, 0, 0,
    };

    // clang-format off
    const StringId kRideInspectionIntervalNames[] = {
        STR_EVERY_10_MINUTES,
        STR_EVERY_20_MINUTES,
        STR_EVERY_30_MINUTES,
        STR_EVERY_45_MINUTES,
        STR_EVERY_HOUR,
        STR_EVERY_2_HOURS,
        STR_NEVER,
    };
    // clang-format on

    // A special instance of Ride that is used to draw previews such as the track designs.
    static Ride _previewRide{};

    struct StationIndexWithMessage
    {
        ::StationIndex StationIndex;
        StringId Message = kStringIdNone;
    };

    // Static function declarations
    Staff* FindClosestMechanic(const CoordsXY& entrancePosition, int32_t forInspection);
    static void RideBreakdownStatusUpdate(Ride& ride);
    static void RideBreakdownUpdate(Ride& ride);
    static void RideCallClosestMechanic(Ride& ride);
    static void RideCallMechanic(Ride& ride, Peep* mechanic, int32_t forInspection);
    static void RideEntranceExitConnected(Ride& ride);
    static Breakdown RideGetNewBreakdownProblem(const Ride& ride);
    static void RideInspectionUpdate(Ride& ride);
    static void RideMechanicStatusUpdate(Ride& ride, MechanicStatus mechanicStatus);
    static void RideMusicUpdate(Ride& ride);
    static void RideShopConnected(const Ride& ride);

    RideId GetNextFreeRideId()
    {
        auto& gameState = getGameState();
        for (RideId::UnderlyingType i = 0; i < gameState.rides.size(); i++)
        {
            if (gameState.rides[i].id.IsNull())
            {
                return RideId::FromUnderlying(i);
            }
        }
        return RideId::GetNull();
    }

    Ride* RideAllocateAtIndex(RideId index)
    {
        const auto idx = index.ToUnderlying();

        auto& gameState = getGameState();
        gameState.ridesEndOfUsedRange = std::max<size_t>(idx + 1, gameState.ridesEndOfUsedRange);

        auto result = &gameState.rides[idx];
        assert(result->id == RideId::GetNull());

        // Initialize the ride to all the defaults.
        *result = Ride{};

        // Because it is default initialized to zero rather than the magic constant for Null, fill the array.
        std::fill(std::begin(result->vehicles), std::end(result->vehicles), EntityId::GetNull());

        result->id = index;
        return result;
    }

    Ride& RideGetTemporaryForPreview()
    {
        return _previewRide;
    }

    static void RideReset(Ride& ride)
    {
        ride.id = RideId::GetNull();
        ride.type = kRideTypeNull;
        ride.customName = {};
        ride.measurement = {};
    }

    void RideDelete(RideId id)
    {
        auto& gameState = getGameState();
        const auto idx = id.ToUnderlying();

        assert(idx < gameState.rides.size());
        assert(gameState.rides[idx].type != kRideTypeNull);

        auto& ride = gameState.rides[idx];
        RideReset(ride);

        // Shrink maximum ride size.
        while (gameState.ridesEndOfUsedRange > 0 && gameState.rides[gameState.ridesEndOfUsedRange - 1].id.IsNull())
        {
            gameState.ridesEndOfUsedRange--;
        }
    }

    Ride* GetRide(RideId index)
    {
        if (index.IsNull())
        {
            return nullptr;
        }

        auto& gameState = getGameState();
        const auto idx = index.ToUnderlying();
        if (idx >= gameState.rides.size())
        {
            return nullptr;
        }

        auto& ride = gameState.rides[idx];
        if (ride.type != kRideTypeNull)
        {
            assert(ride.id == index);
            return &ride;
        }

        return nullptr;
    }

    const RideObjectEntry* GetRideEntryByIndex(ObjectEntryIndex index)
    {
        auto& objMgr = GetContext()->GetObjectManager();

        auto obj = objMgr.GetLoadedObject<RideObject>(index);
        if (obj == nullptr)
        {
            return nullptr;
        }

        return static_cast<RideObjectEntry*>(obj->GetLegacyData());
    }

    std::string_view GetRideEntryName(ObjectEntryIndex index)
    {
        if (index >= getObjectEntryGroupCount(ObjectType::ride))
        {
            LOG_ERROR("invalid index %d for ride type", index);
            return {};
        }

        auto objectEntry = ObjectEntryGetObject(ObjectType::ride, index);
        if (objectEntry != nullptr)
        {
            return objectEntry->GetLegacyIdentifier();
        }
        return {};
    }

    const RideObjectEntry* Ride::getRideEntry() const
    {
        return GetRideEntryByIndex(subtype);
    }

    int32_t RideGetCount()
    {
        auto& gameState = getGameState();
        return static_cast<int32_t>(RideManager(gameState).size());
    }

    size_t Ride::getNumPrices() const
    {
        size_t result = 0;
        const auto& rtd = getRideTypeDescriptor();
        if (rtd.specialType == RtdSpecialType::cashMachine || rtd.specialType == RtdSpecialType::firstAid)
        {
            result = 0;
        }
        else if (rtd.specialType == RtdSpecialType::toilet)
        {
            result = 1;
        }
        else
        {
            result = 1;

            auto rideEntry = getRideEntry();
            if (rideEntry != nullptr)
            {
                if (flags.has(RideFlag::onRidePhoto))
                {
                    result++;
                }
                else if (rideEntry->shop_item[1] != ShopItem::none)
                {
                    result++;
                }
            }
        }
        return result;
    }

    int32_t Ride::getAge() const
    {
        return GetDate().GetMonthsElapsed() - buildDate;
    }

    int32_t Ride::getTotalQueueLength() const
    {
        int32_t queueLength = 0;
        for (const auto& station : stations)
            if (!station.Entrance.IsNull())
                queueLength += station.QueueLength;
        return queueLength;
    }

    int32_t Ride::getMaxQueueTime() const
    {
        uint8_t queueTime = 0;
        for (const auto& station : stations)
            if (!station.Entrance.IsNull())
                queueTime = std::max(queueTime, station.QueueTime);
        return static_cast<int32_t>(queueTime);
    }

    Guest* Ride::getQueueHeadGuest(StationIndex stationIndex) const
    {
        Guest* peep;
        Guest* result = nullptr;
        auto spriteIndex = getStation(stationIndex).LastPeepInQueue;
        while ((peep = getGameState().entities.tryGetEntity<Guest>(spriteIndex)) != nullptr)
        {
            spriteIndex = peep->guestNextInQueue;
            result = peep;
        }
        return result;
    }

    void Ride::updateQueueLength(StationIndex stationIndex)
    {
        uint16_t count = 0;
        Guest* peep;
        auto& station = getStation(stationIndex);
        auto spriteIndex = station.LastPeepInQueue;
        while ((peep = getGameState().entities.tryGetEntity<Guest>(spriteIndex)) != nullptr)
        {
            spriteIndex = peep->guestNextInQueue;
            count++;
        }
        station.QueueLength = count;
    }

    void Ride::queueInsertGuestAtFront(StationIndex stationIndex, Guest* peep)
    {
        assert(stationIndex.ToUnderlying() < Limits::kMaxStationsPerRide);
        assert(peep != nullptr);

        peep->guestNextInQueue = EntityId::GetNull();
        auto* queueHeadGuest = getQueueHeadGuest(peep->currentRideStation);
        if (queueHeadGuest == nullptr)
        {
            getStation(peep->currentRideStation).LastPeepInQueue = peep->id;
        }
        else
        {
            queueHeadGuest->guestNextInQueue = peep->id;
        }
        updateQueueLength(peep->currentRideStation);
    }

    /**
     *
     *  rct2: 0x006AC916
     */
    void RideUpdateFavouritedStat()
    {
        auto& gameState = getGameState();
        for (auto& ride : RideManager(gameState))
            ride.guestsFavourite = 0;

        for (auto peep : EntityList<Guest>())
        {
            if (!peep->favouriteRide.IsNull())
            {
                auto ride = GetRide(peep->favouriteRide);
                if (ride != nullptr)
                {
                    ride->guestsFavourite = AddClamp(ride->guestsFavourite, 1u);
                    ride->windowInvalidateFlags.set(RideInvalidateFlag::customers);
                }
            }
        }

        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->InvalidateByClass(WindowClass::rideList);
    }

    /**
     *
     *  rct2: 0x006AC3AB
     */
    money64 Ride::calculateIncomePerHour() const
    {
        // Get entry by ride to provide better reporting
        const auto* entry = getRideEntry();
        if (entry == nullptr)
        {
            return 0;
        }
        auto customersPerHour = RideCustomersPerHour(*this);
        money64 priceMinusCost = RideGetPrice(*this);

        ShopItem currentShopItem = entry->shop_item[0];
        if (currentShopItem != ShopItem::none)
        {
            priceMinusCost -= GetShopItemDescriptor(currentShopItem).Cost;
        }

        currentShopItem = flags.has(RideFlag::onRidePhoto) ? getRideTypeDescriptor().PhotoItem : entry->shop_item[1];

        if (currentShopItem != ShopItem::none)
        {
            const money64 shopItemProfit = price[1] - GetShopItemDescriptor(currentShopItem).Cost;

            if (GetShopItemDescriptor(currentShopItem).IsPhoto())
            {
                const int32_t rideTicketsSold = totalCustomers - numSecondaryItemsSold;

                // Use the ratio between photo sold and total admissions to approximate the photo income(as not every guest will
                // buy one).
                // TODO: use data from the last 5 minutes instead of all-time values for a more accurate calculation
                if (rideTicketsSold > 0)
                {
                    priceMinusCost += ((static_cast<int32_t>(numSecondaryItemsSold) * shopItemProfit) / rideTicketsSold);
                }
            }
            else
            {
                priceMinusCost += shopItemProfit;
            }

            if (entry->shop_item[0] != ShopItem::none)
                priceMinusCost /= 2;
        }

        return customersPerHour * priceMinusCost;
    }

    /**
     *
     *  rct2: 0x006CAF80
     * ax result x
     * bx result y
     * dl ride index
     * esi result map element
     */
    bool RideTryGetOriginElement(const Ride& ride, CoordsXYE* output)
    {
        TileElement* resultTileElement = nullptr;

        TileElementIterator it;
        TileElementIteratorBegin(&it);
        do
        {
            if (it.element->getType() != TileElementType::track)
                continue;
            if (it.element->asTrack()->getRideIndex() != ride.id)
                continue;

            // Found a track piece for target ride

            // Check if it's not the station or ??? (but allow end piece of station)
            const auto& ted = GetTrackElementDescriptor(it.element->asTrack()->getTrackType());
            bool specialTrackPiece
                = (it.element->asTrack()->getTrackType() != TrackElemType::beginStation
                   && it.element->asTrack()->getTrackType() != TrackElemType::middleStation
                   && ted.sequenceData.sequences[0].flags.has(SequenceFlag::trackOrigin));

            // 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 * kCoordsXYStep;
                    output->y = it.y * kCoordsXYStep;
                }
            }

            if (specialTrackPiece)
            {
                return true;
            }
        } while (TileElementIteratorNext(&it));

        return resultTileElement != nullptr;
    }

    void Ride::formatStatusTo(Formatter& ft) const
    {
        if (flags.has(RideFlag::crashed))
        {
            ft.Add<StringId>(STR_CRASHED);
        }
        else if (flags.has(RideFlag::brokenDown))
        {
            ft.Add<StringId>(STR_BROKEN_DOWN);
        }
        else if (status == RideStatus::closed)
        {
            if (!getRideTypeDescriptor().flags.has(RtdFlag::isShopOrFacility))
            {
                if (numRiders != 0)
                {
                    ft.Add<StringId>(numRiders == 1 ? STR_CLOSED_WITH_PERSON : STR_CLOSED_WITH_PEOPLE);
                    ft.Add<uint16_t>(numRiders);
                }
                else
                {
                    ft.Add<StringId>(STR_CLOSED);
                }
            }
            else
            {
                ft.Add<StringId>(STR_CLOSED);
            }
        }
        else if (status == RideStatus::simulating)
        {
            ft.Add<StringId>(STR_SIMULATING);
        }
        else if (status == RideStatus::testing)
        {
            ft.Add<StringId>(STR_TEST_RUN);
        }
        else if (mode == RideMode::race && !flags.has(RideFlag::passStationNoStopping) && !raceWinner.IsNull())
        {
            auto peep = getGameState().entities.getEntity<Guest>(raceWinner);
            if (peep != nullptr)
            {
                ft.Add<StringId>(STR_RACE_WON_BY);
                peep->formatNameTo(ft);
            }
            else
            {
                ft.Add<StringId>(STR_RACE_WON_BY);
                ft.Add<StringId>(kStringIdNone);
            }
        }
        else if (!getRideTypeDescriptor().flags.has(RtdFlag::isShopOrFacility))
        {
            ft.Add<StringId>(numRiders == 1 ? STR_PERSON_ON_RIDE : STR_PEOPLE_ON_RIDE);
            ft.Add<uint16_t>(numRiders);
        }
        else
        {
            ft.Add<StringId>(STR_OPEN);
        }
    }

    int32_t Ride::getTotalLength() const
    {
        int32_t totalLength = 0;
        for (int32_t i = 0; i < numStations; i++)
            totalLength += stations[i].SegmentLength;
        return totalLength;
    }

    int32_t Ride::getTotalTime() const
    {
        int32_t totalTime = 0;
        for (int32_t i = 0; i < numStations; i++)
            totalTime += stations[i].SegmentTime;
        return totalTime;
    }

    bool Ride::canHaveMultipleCircuits() const
    {
        if (!getRideTypeDescriptor().flags.has(RtdFlag::allowMultipleCircuits))
            return false;

        // Only allow circuit or launch modes
        if (mode != RideMode::continuousCircuit && mode != RideMode::reverseInclineLaunchedShuttle
            && mode != RideMode::poweredLaunchPassthrough)
        {
            return false;
        }

        // Must have no more than one vehicle and one station
        if (numTrains > 1 || numStations > 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.flags.has(RtdFlag::noTestMode) && rtd.flags.has(RtdFlag::hasTrack));
            case RideStatus::testing:
                return !rtd.flags.has(RtdFlag::noTestMode);
            case RideStatus::count: // Meaningless but necessary to satisfy -Wswitch
                return false;
        }
        // Unreachable
        return false;
    }

    bool Ride::hasFailingBrakes() const
    {
        return flags.has(RideFlag::brokenDown) && breakdownReasonPending == Breakdown::brakesFailure
            && mechanicStatus != MechanicStatus::hasFixedStationBrakes;
    }

#pragma region Initialisation functions

    /**
     *
     *  rct2: 0x006ACA89
     */
    void RideInitAll()
    {
        auto& gameState = getGameState();
        std::for_each(std::begin(gameState.rides), std::end(gameState.rides), RideReset);
        gameState.ridesEndOfUsedRange = 0;
    }

    /**
     *
     *  rct2: 0x006B7A38
     */
    void ResetAllRideBuildDates()
    {
        auto& gameState = getGameState();
        for (auto& ride : RideManager(gameState))
        {
            ride.buildDate -= GetDate().GetMonthsElapsed();
        }
    }

#pragma endregion

#pragma region Construction

#pragma endregion

#pragma region Update functions

    /**
     *
     *  rct2: 0x006ABE4C
     */
    void Ride::updateAll()
    {
        PROFILED_FUNCTION();

        auto& gameState = getGameState();

        // Remove all rides if scenario editor
        if (gLegacyScene == LegacyScene::scenarioEditor)
        {
            switch (getGameState().editorStep)
            {
                case Editor::Step::objectSelection:
                case Editor::Step::landscapeEditor:
                case Editor::Step::inventionsListSetUp:
                {
                    for (auto& ride : RideManager(gameState))
                        ride.remove();
                    break;
                }
                case Editor::Step::optionsSelection:
                case Editor::Step::objectiveSelection:
                case Editor::Step::scenarioDetails:
                case Editor::Step::saveScenario:
                case Editor::Step::rollerCoasterDesigner:
                case Editor::Step::designsManager:
                case Editor::Step::invalid:
                    break;
            }
            return;
        }

        WindowUpdateViewportRideMusic();

        // Update rides
        for (auto& ride : RideManager(gameState))
            ride.update();

        RideAudio::UpdateMusicChannels();
    }

    std::unique_ptr<TrackDesign> Ride::saveToTrackDesign(TrackDesignState& tds) const
    {
        if (!flags.has(RideFlag::tested))
        {
            ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, kStringIdNone, {});
            return nullptr;
        }

        if (!RideHasRatings(*this))
        {
            ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, kStringIdNone, {});
            return nullptr;
        }

        auto td = std::make_unique<TrackDesign>();
        auto errMessage = td->CreateTrackDesign(tds, *this);
        if (!errMessage.Successful)
        {
            ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, errMessage.Message, {});
            return nullptr;
        }
        if (errMessage.HasMessage())
        {
            ContextShowError(errMessage.Message, kStringIdEmpty, {});
        }

        return td;
    }

    RideStation& Ride::getStation(StationIndex stationIndex)
    {
        return stations[stationIndex.ToUnderlying()];
    }

    StationIndex::UnderlyingType Ride::getStationNumber(StationIndex in) const
    {
        StationIndex::UnderlyingType nullStationsSeen{ 0 };
        for (size_t i = 0; i < in.ToUnderlying(); i++)
        {
            if (stations[i].Start.IsNull())
            {
                nullStationsSeen++;
            }
        }

        return in.ToUnderlying() - nullStationsSeen + 1;
    }

    const RideStation& Ride::getStation(StationIndex stationIndex) const
    {
        return stations[stationIndex.ToUnderlying()];
    }

    std::span<RideStation> Ride::getStations()
    {
        return stations;
    }

    std::span<const RideStation> Ride::getStations() const
    {
        return stations;
    }

    StationIndex Ride::getStationIndex(const RideStation* station) const
    {
        auto distance = std::distance(stations.data(), station);
        Guard::Assert(distance >= 0 && distance < int32_t(std::size(stations)));
        return StationIndex::FromUnderlying(distance);
    }

    /**
     *
     *  rct2: 0x006ABE73
     */
    void Ride::update()
    {
        if (vehicleChangeTimeout != 0)
            vehicleChangeTimeout--;

        RideMusicUpdate(*this);

        // Update stations
        const auto& rtd = getRideTypeDescriptor();
        if (rtd.specialType != RtdSpecialType::maze)
            for (StationIndex::UnderlyingType i = 0; i < Limits::kMaxStationsPerRide; i++)
                RideUpdateStation(*this, StationIndex::FromUnderlying(i));

        // Update financial statistics
        numCustomersTimeout++;

        if (numCustomersTimeout >= 960)
        {
            // This is meant to update about every 30 seconds
            numCustomersTimeout = 0;

            // Shift number of customers history, start of the array is the most recent one
            for (int32_t i = Limits::kCustomerHistorySize - 1; i > 0; i--)
            {
                numCustomers[i] = numCustomers[i - 1];
            }
            numCustomers[0] = curNumCustomers;

            curNumCustomers = 0;
            windowInvalidateFlags.set(RideInvalidateFlag::customers);

            incomePerHour = calculateIncomePerHour();
            windowInvalidateFlags.set(RideInvalidateFlag::income);

            if (upkeepCost != kMoney64Undefined)
                profit = incomePerHour - (upkeepCost * 16);
        }

        // Ride specific updates
        if (rtd.RideUpdate != nullptr)
            rtd.RideUpdate(*this);

        RideBreakdownUpdate(*this);

        // Various things include news messages
        if (flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown, RideFlag::dueInspection))
        {
            // Breakdown updates originally were performed when (id == (gCurrentTicks / 2) & 0xFF)
            // with the increased MAX_RIDES the update is tied to the first byte of the id this allows
            // for identical balance with vanilla.
            const auto updatingRideByte = static_cast<uint8_t>((getGameState().currentTicks / 2) & 0xFF);
            if (updatingRideByte == static_cast<uint8_t>(id.ToUnderlying()))
                RideBreakdownStatusUpdate(*this);
        }

        RideInspectionUpdate(*this);

        // If ride is simulating but crashed, reset the vehicles
        if (status == RideStatus::simulating && flags.has(RideFlag::crashed))
        {
            // We require this to execute right away during the simulation, always ignore network and queue.
            auto gameAction = GameActions::RideSetStatusAction(id, RideStatus::simulating);
            GameActions::ExecuteNested(&gameAction, getGameState());
        }
    }

    /**
     *
     *  rct2: 0x006AC489
     */
    void updateChairlift(Ride& ride)
    {
        if (!ride.flags.has(RideFlag::onTrack))
            return;
        if (ride.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown, RideFlag::crashed)
            && ride.breakdownReasonPending == Breakdown::safetyCutOut)
            return;

        uint16_t oldChairliftBullwheelRotation = ride.chairliftBullwheelRotation >> 14;
        ride.chairliftBullwheelRotation += ride.speed * 2048;
        if (oldChairliftBullwheelRotation == ride.speed / 8)
            return;

        auto bullwheelLoc = ride.chairliftBullwheelLocation[0].ToCoordsXYZ();
        MapInvalidateTileZoom1({ bullwheelLoc, bullwheelLoc.z, bullwheelLoc.z + (4 * kCoordsZStep) });

        bullwheelLoc = ride.chairliftBullwheelLocation[1].ToCoordsXYZ();
        MapInvalidateTileZoom1({ bullwheelLoc, bullwheelLoc.z, bullwheelLoc.z + (4 * kCoordsZStep) });
    }

    /**
     *
     *  rct2: 0x0069A3A2
     * edi: ride (in code as bytes offset from start of rides list)
     * bl: happiness
     */
    void Ride::updateSatisfaction(const uint8_t happiness)
    {
        satisfactionNext += happiness;
        satisfactionTimeout++;
        if (satisfactionTimeout >= 20)
        {
            satisfaction = satisfactionNext >> 2;
            satisfactionNext = 0;
            satisfactionTimeout = 0;
            windowInvalidateFlags.set(RideInvalidateFlag::customers);
        }
    }

    /**
     *
     *  rct2: 0x0069A3D7
     * Updates the ride popularity
     * edi : ride
     * bl  : pop_amount
     * pop_amount can be zero if peep visited but did not purchase.
     */
    void Ride::updatePopularity(const uint8_t pop_amount)
    {
        popularityNext += pop_amount;
        popularityTimeout++;
        if (popularityTimeout < 25)
            return;

        popularity = popularityNext;
        popularityNext = 0;
        popularityTimeout = 0;
        windowInvalidateFlags.set(RideInvalidateFlag::customers);
    }

    /** rct2: 0x0098DDB8, 0x0098DDBA */
    static constexpr CoordsXY ride_spiral_slide_main_tile_offset[][4] = {
        {
            { 32, 32 },
            { 0, 32 },
            { 0, 0 },
            { 32, 0 },
        },
        {
            { 32, 0 },
            { 0, 0 },
            { 0, -32 },
            { 32, -32 },
        },
        {
            { 0, 0 },
            { -32, 0 },
            { -32, -32 },
            { 0, -32 },
        },
        {
            { 0, 0 },
            { 0, 32 },
            { -32, 32 },
            { -32, 0 },
        },
    };

    /**
     *
     *  rct2: 0x006AC545
     */

    void updateSpiralSlide(Ride& ride)
    {
        if (getGameState().currentTicks & 3)
            return;
        if (!ride.slideInUse)
            return;

        ride.spiralSlideProgress++;
        if (ride.spiralSlideProgress >= 48)
        {
            ride.slideInUse = 0;

            auto* peep = getGameState().entities.getEntity<Guest>(ride.slidePeep);
            if (peep != nullptr)
            {
                peep->spiralSlideSubstate = PeepSpiralSlideSubState::finishedSliding;
            }
        }

        const uint8_t current_rotation = GetCurrentRotation();
        // Invalidate something related to station start
        for (int32_t i = 0; i < Limits::kMaxStationsPerRide; i++)
        {
            if (ride.stations[i].Start.IsNull())
                continue;

            auto startLoc = ride.stations[i].Start;

            TileElement* tileElement = RideGetStationStartTrackElement(ride, StationIndex::FromUnderlying(i));
            if (tileElement == nullptr)
                continue;

            int32_t rotation = tileElement->getDirection();
            startLoc += ride_spiral_slide_main_tile_offset[rotation][current_rotation];

            MapInvalidateTileZoom0({ startLoc, tileElement->getBaseZ(), tileElement->getClearanceZ() });
        }
    }

#pragma endregion

#pragma region Breakdown and inspection functions

    static uint8_t _breakdownProblemProbabilities[] = {
        25, // Breakdown::safetyCutOut
        12, // Breakdown::restraintsStuckClosed
        10, // Breakdown::restraintsStuckOpen
        13, // Breakdown::doorsStuckClosed
        10, // Breakdown::doorsStuckOpen
        6,  // Breakdown::vehicleMalfunction
        0,  // Breakdown::brakesFailure
        3,  // Breakdown::controlFailure
    };

    /**
     *
     *  rct2: 0x006AC7C2
     */
    static void RideInspectionUpdate(Ride& ride)
    {
        if (getGameState().currentTicks & 2047)
            return;
        if (gLegacyScene == LegacyScene::trackDesigner)
            return;

        ride.lastInspection = AddClamp<decltype(ride.lastInspection)>(ride.lastInspection, 1);
        ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);

        int32_t inspectionIntervalMinutes = RideInspectionInterval[EnumValue(ride.inspectionInterval)];
        // An inspection interval of 0 minutes means the ride is set to never be inspected.
        if (inspectionIntervalMinutes == 0)
        {
            ride.flags.unset(RideFlag::dueInspection);
            return;
        }

        if (ride.getRideTypeDescriptor().availableBreakdowns.isEmpty())
            return;

        if (inspectionIntervalMinutes > ride.lastInspection)
            return;

        if (ride.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown, RideFlag::dueInspection, RideFlag::crashed))
            return;

        // Inspect the first station that has an exit
        ride.flags.set(RideFlag::dueInspection);
        ride.mechanicStatus = MechanicStatus::calling;

        auto stationIndex = RideGetFirstValidStationExit(ride);
        ride.inspectionStation = (!stationIndex.IsNull()) ? stationIndex : StationIndex::FromUnderlying(0);
    }

    static int32_t getAgePenalty(const Ride& ride)
    {
        auto years = DateGetYear(ride.getAge());
        switch (years)
        {
            case 0:
                return 0;
            case 1:
                return ride.unreliabilityFactor / 8;
            case 2:
                return ride.unreliabilityFactor / 4;
            case 3:
            case 4:
                return ride.unreliabilityFactor / 2;
            case 5:
            case 6:
            case 7:
                return ride.unreliabilityFactor;
            default:
                return ride.unreliabilityFactor * 2;
        }
    }

    /**
     *
     *  rct2: 0x006AC622
     */
    static void RideBreakdownUpdate(Ride& ride)
    {
        auto& gameState = getGameState();
        const auto currentTicks = gameState.currentTicks;
        if (currentTicks & 255)
            return;

        if (gLegacyScene == LegacyScene::trackDesigner)
            return;

        if (ride.flags.hasAny(RideFlag::brokenDown, RideFlag::crashed))
            ride.downtimeHistory[0]++;

        if (!(currentTicks & 8191))
        {
            int32_t totalDowntime = 0;

            for (int32_t i = 0; i < Limits::kDowntimeHistorySize; i++)
            {
                totalDowntime += ride.downtimeHistory[i];
            }

            ride.downtime = std::min(totalDowntime / 2, 100);

            for (int32_t i = Limits::kDowntimeHistorySize - 1; i > 0; i--)
            {
                ride.downtimeHistory[i] = ride.downtimeHistory[i - 1];
            }
            ride.downtimeHistory[0] = 0;

            ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);
        }

        if (ride.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown, RideFlag::crashed))
            return;
        if (ride.status == RideStatus::closed || ride.status == RideStatus::simulating)
            return;

        if (!ride.canBreakDown())
        {
            ride.reliability = kRideInitialReliability;
            return;
        }

        // Calculate breakdown probability?
        int32_t unreliabilityAccumulator = ride.unreliabilityFactor + getAgePenalty(ride);
        ride.reliability = static_cast<uint16_t>(std::max(0, (ride.reliability - unreliabilityAccumulator)));
        ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);

        // Random probability of a breakdown. Roughly this is 1 in
        //
        // (25000 - reliability) / 3 000 000
        //
        // a 0.8% chance, less the breakdown factor which accumulates as the game
        // continues.
        if ((ride.reliability == 0
             || static_cast<uint32_t>(ScenarioRand() & 0x2FFFFF) <= 1u + kRideInitialReliability - ride.reliability)
            && !gameState.cheats.disableAllBreakdowns)
        {
            auto breakdownReason = RideGetNewBreakdownProblem(ride);
            if (breakdownReason != Breakdown::none)
                RidePrepareBreakdown(ride, breakdownReason);
        }
    }

    /**
     *
     *  rct2: 0x006B7294
     */
    static Breakdown RideGetNewBreakdownProblem(const Ride& ride)
    {
        // Brake failure is more likely when it's raining or heavily snowing (HeavySnow and Blizzard)
        _breakdownProblemProbabilities[EnumValue(Breakdown::brakesFailure)] = Weather::isPrecipitating() ? 20 : 3;

        if (!ride.canBreakDown())
            return Breakdown::none;

        auto availableBreakdownProblems = ride.getRideTypeDescriptor().availableBreakdowns;

        // Calculate the total probability range for all possible breakdown problems
        int32_t totalProbability = 0;
        for (Breakdown breakdown : kAllBreakdownTypes)
        {
            if (availableBreakdownProblems.has(breakdown))
                totalProbability += _breakdownProblemProbabilities[EnumValue(breakdown)];
        }

        if (totalProbability == 0)
            return Breakdown::none;

        // Choose a random number within this range
        int32_t randomProbability = ScenarioRand() % totalProbability;

        // Find which problem range the random number lies
        auto breakdownProblem = Breakdown::none;
        for (Breakdown breakdown : kAllBreakdownTypes)
        {
            if (availableBreakdownProblems.has(breakdown))
            {
                breakdownProblem = breakdown;
                randomProbability -= _breakdownProblemProbabilities[EnumValue(breakdown)];
            }
            if (randomProbability < 0)
                break;
        }

        if (breakdownProblem != Breakdown::brakesFailure)
            return breakdownProblem;

        // Brakes failure can not happen if block brakes are used (so long as there is more than one vehicle)
        // However if this is the case, brake failure should be taken out the equation, otherwise block brake
        // rides have a lower probability to break down due to a random implementation reason.
        if (ride.isBlockSectioned())
            if (ride.numTrains != 1)
                return Breakdown::none;

        // If brakes failure is disabled, also take it out of the equation (see above comment why)
        if (getGameState().cheats.disableBrakesFailure)
            return Breakdown::none;

        auto monthsOld = ride.getAge();
        if (monthsOld < 16 || ride.reliabilityPercentage > 50)
            return Breakdown::none;

        return Breakdown::brakesFailure;
    }

    bool Ride::canBreakDown() const
    {
        if (getRideTypeDescriptor().availableBreakdowns.isEmpty())
        {
            return false;
        }

        const auto* entry = getRideEntry();
        return entry != nullptr && !entry->flags.has(RideEntryFlag::cannotBreakDown);
    }

    static void ChooseRandomTrainToBreakdownSafe(Ride& ride)
    {
        // Prevent integer division by zero in case of hacked ride.
        if (ride.numTrains == 0)
            return;

        ride.brokenTrain = ScenarioRand() % ride.numTrains;

        // Prevent crash caused by accessing SPRITE_INDEX_NULL on hacked rides.
        // This should probably be cleaned up on import instead.
        while (ride.vehicles[ride.brokenTrain].IsNull() && ride.brokenTrain != 0)
        {
            --ride.brokenTrain;
        }
    }

    /**
     *
     *  rct2: 0x006B7348
     */
    void RidePrepareBreakdown(Ride& ride, Breakdown breakdownReason)
    {
        StationIndex i;
        Vehicle* vehicle;

        if (ride.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown, RideFlag::crashed))
            return;

        ride.flags.set(RideFlag::breakdownPending);

        ride.breakdownReasonPending = breakdownReason;
        ride.breakdownSoundModifier = 0;
        ride.notFixedTimeout = 0;
        ride.inspectionStation = StationIndex::FromUnderlying(0); // ensure set to something.

        switch (breakdownReason)
        {
            case Breakdown::safetyCutOut:
            case Breakdown::controlFailure:
                // Inspect first station with an exit
                i = RideGetFirstValidStationExit(ride);
                if (!i.IsNull())
                {
                    ride.inspectionStation = i;
                }

                break;
            case Breakdown::restraintsStuckClosed:
            case Breakdown::restraintsStuckOpen:
            case Breakdown::doorsStuckClosed:
            case Breakdown::doorsStuckOpen:
                // Choose a random train and car
                ChooseRandomTrainToBreakdownSafe(ride);
                if (ride.numCarsPerTrain != 0)
                {
                    ride.brokenCar = ScenarioRand() % ride.numCarsPerTrain;

                    // Set flag on broken car
                    vehicle = getGameState().entities.getEntity<Vehicle>(ride.vehicles[ride.brokenTrain]);
                    if (vehicle != nullptr)
                    {
                        vehicle = vehicle->GetCar(ride.brokenCar);
                    }
                    if (vehicle != nullptr)
                    {
                        vehicle->flags.set(VehicleFlag::carIsBroken);
                    }
                }
                break;
            case Breakdown::vehicleMalfunction:
                // Choose a random train
                ChooseRandomTrainToBreakdownSafe(ride);
                ride.brokenCar = 0;

                // Set flag on broken train, first car
                vehicle = getGameState().entities.getEntity<Vehicle>(ride.vehicles[ride.brokenTrain]);
                if (vehicle != nullptr)
                {
                    vehicle->flags.set(VehicleFlag::trainIsBroken);
                }
                break;
            case Breakdown::brakesFailure:
                // Original code generates a random number but does not use it
                // Unsure if this was supposed to choose a random station (or random station with an exit)
                i = RideGetFirstValidStationExit(ride);
                if (!i.IsNull())
                {
                    ride.inspectionStation = i;
                }
                break;
            default:
                break;
        }
#ifdef ENABLE_SCRIPTING
        auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine();
        if (hookEngine.HasSubscriptions(HookType::rideBreakDown))
        {
            auto ctx = GetContext()->GetScriptEngine().GetContext();
            JSValue obj = JS_NewObject(ctx);

            JS_SetPropertyStr(ctx, obj, "rideId", JS_NewInt32(ctx, ride.id.ToUnderlying()));

            auto it = kBreakdownMap.find(breakdownReason);
            if (it != kBreakdownMap.end())
                JS_SetPropertyStr(ctx, obj, "breakdownReason", JS_NewString(ctx, it->first.data()));
            else
                JS_SetPropertyStr(ctx, obj, "breakdownReason", JS_NewString(ctx, "none"));

            hookEngine.Call(HookType::rideBreakDown, obj, true);
        }
#endif
    }

    /**
     *
     *  rct2: 0x006B74FA
     */
    void RideBreakdownAddNewsItem(const Ride& ride)
    {
        if (Config::Get().notifications.rideBrokenDown)
        {
            Formatter ft;
            ride.formatNameTo(ft);
            News::AddItemToQueue(News::ItemType::ride, STR_RIDE_IS_BROKEN_DOWN, ride.id.ToUnderlying(), ft);
        }
    }

    /**
     *
     *  rct2: 0x006B75C8
     */
    static void RideBreakdownStatusUpdate(Ride& ride)
    {
        // Warn player if ride hasn't been fixed for ages
        if (ride.flags.has(RideFlag::brokenDown))
        {
            ride.notFixedTimeout++;
            // 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.notFixedTimeout == 0)
                ride.notFixedTimeout -= 16;

            if (!(ride.notFixedTimeout & 15) && ride.mechanicStatus != MechanicStatus::fixing
                && ride.mechanicStatus != MechanicStatus::hasFixedStationBrakes)
            {
                if (Config::Get().notifications.rideWarnings)
                {
                    Formatter ft;
                    ride.formatNameTo(ft);
                    News::AddItemToQueue(News::ItemType::ride, STR_RIDE_IS_STILL_NOT_FIXED, ride.id.ToUnderlying(), ft);
                }
            }
        }

        RideMechanicStatusUpdate(ride, ride.mechanicStatus);
    }

    /**
     *
     *  rct2: 0x006B762F
     */
    static void RideMechanicStatusUpdate(Ride& ride, MechanicStatus mechanicStatus)
    {
        // Turn a pending breakdown into a breakdown.
        if ((mechanicStatus == MechanicStatus::undefined || mechanicStatus == MechanicStatus::calling
             || mechanicStatus == MechanicStatus::heading)
            && ride.flags.has(RideFlag::breakdownPending) && !ride.flags.has(RideFlag::brokenDown))
        {
            auto breakdownReason = ride.breakdownReasonPending;
            if (breakdownReason == Breakdown::safetyCutOut || breakdownReason == Breakdown::brakesFailure
                || breakdownReason == Breakdown::controlFailure)
            {
                ride.flags.set(RideFlag::brokenDown);
                ride.windowInvalidateFlags.set(
                    RideInvalidateFlag::maintenance, RideInvalidateFlag::list, RideInvalidateFlag::main);
                ride.breakdownReason = breakdownReason;
                RideBreakdownAddNewsItem(ride);
            }
        }
        switch (mechanicStatus)
        {
            case MechanicStatus::undefined:
                if (ride.flags.has(RideFlag::brokenDown))
                {
                    ride.mechanicStatus = MechanicStatus::calling;
                }
                break;
            case MechanicStatus::calling:
                if (ride.getRideTypeDescriptor().availableBreakdowns.isEmpty())
                {
                    ride.flags.unset(RideFlag::breakdownPending, RideFlag::brokenDown, RideFlag::dueInspection);
                    break;
                }

                RideCallClosestMechanic(ride);
                break;
            case MechanicStatus::heading:
            {
                auto mechanic = RideGetMechanic(ride);
                bool rideNeedsRepair = ride.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown);
                if (mechanic == nullptr
                    || (mechanic->state != PeepState::headingToInspection && mechanic->state != PeepState::answering)
                    || mechanic->currentRide != ride.id)
                {
                    ride.mechanicStatus = MechanicStatus::calling;
                    ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);
                    RideMechanicStatusUpdate(ride, MechanicStatus::calling);
                }
                // if the ride is broken down, but a mechanic was heading for an inspection, update orders to fix
                else if (rideNeedsRepair && mechanic->state == PeepState::headingToInspection)
                {
                    // updates orders for mechanic already heading to inspect ride
                    // forInspection == false means start repair (goes to PeepState::answering)
                    RideCallMechanic(ride, mechanic, false);
                }
                break;
            }
            case MechanicStatus::fixing:
            {
                auto mechanic = RideGetMechanic(ride);
                if (mechanic == nullptr
                    || (mechanic->state != PeepState::headingToInspection && mechanic->state != PeepState::fixing
                        && mechanic->state != PeepState::inspecting && mechanic->state != PeepState::answering))
                {
                    ride.mechanicStatus = MechanicStatus::calling;
                    ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);
                    RideMechanicStatusUpdate(ride, MechanicStatus::calling);
                }
                break;
            }
            default:
                break;
        }
    }

    /**
     *
     *  rct2: 0x006B796C
     */
    static void RideCallMechanic(Ride& ride, Peep* mechanic, int32_t forInspection)
    {
        mechanic->setState(forInspection ? PeepState::headingToInspection : PeepState::answering);
        mechanic->subState = 0;
        ride.mechanicStatus = MechanicStatus::heading;
        ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);
        ride.mechanic = mechanic->id;
        mechanic->currentRide = ride.id;
        mechanic->currentRideStation = ride.inspectionStation;
    }

    /**
     *
     *  rct2: 0x006B76AB
     */
    static void RideCallClosestMechanic(Ride& ride)
    {
        auto forInspection = !ride.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown);
        auto mechanic = RideFindClosestMechanic(ride, forInspection);
        if (mechanic != nullptr)
            RideCallMechanic(ride, mechanic, forInspection);
    }

    Staff* RideFindClosestMechanic(const Ride& ride, int32_t forInspection)
    {
        const auto stationIndex = ride.inspectionStation.IsNull() ? RideGetFirstValidStationExit(ride) : ride.inspectionStation;
        if (stationIndex.IsNull())
            return nullptr;

        // Get either exit position or entrance position if there is no exit
        const auto& station = ride.getStation(stationIndex);
        TileCoordsXYZD location = station.Exit;
        if (location.IsNull())
        {
            location = station.Entrance;
            if (station.Entrance.IsNull())
                return nullptr;
        }

        // Get station start track element and position
        auto mapLocation = location.ToCoordsXYZ();
        TileElement* tileElement = RideGetStationExitElement(mapLocation);
        if (tileElement == nullptr)
            return nullptr;

        // Set x,y to centre of the station exit for the mechanic search.
        auto centreMapLocation = mapLocation.ToTileCentre();

        return FindClosestMechanic(centreMapLocation, forInspection);
    }

    /**
     *
     *  rct2: 0x006B774B (forInspection = 0)
     *  rct2: 0x006B78C3 (forInspection = 1)
     */
    Staff* FindClosestMechanic(const CoordsXY& entrancePosition, int32_t forInspection)
    {
        Staff* closestMechanic = nullptr;
        uint32_t closestDistance = std::numeric_limits<uint32_t>::max();

        for (auto peep : EntityList<Staff>())
        {
            if (!peep->isMechanic())
                continue;

            if (!forInspection)
            {
                if (peep->state == PeepState::headingToInspection)
                {
                    if (peep->subState >= 4)
                        continue;
                }
                else if (peep->state != PeepState::patrolling)
                    continue;

                if (!(peep->staffOrders & STAFF_ORDERS_FIX_RIDES))
                    continue;
            }
            else
            {
                if (peep->state != PeepState::patrolling || !(peep->staffOrders & STAFF_ORDERS_INSPECT_RIDES))
                    continue;
            }

            auto location = entrancePosition.ToTileStart();
            if (MapIsLocationInPark(location))
                if (!peep->isLocationInPatrol(location))
                    continue;

            if (peep->x == kLocationNull)
                continue;

            // Manhattan distance
            uint32_t distance = std::abs(peep->x - entrancePosition.x) + std::abs(peep->y - entrancePosition.y);
            if (distance < closestDistance)
            {
                closestDistance = distance;
                closestMechanic = peep;
            }
        }

        return closestMechanic;
    }

    Staff* RideGetMechanic(const Ride& ride)
    {
        auto staff = getGameState().entities.getEntity<Staff>(ride.mechanic);
        if (staff != nullptr && staff->isMechanic())
        {
            return staff;
        }
        return nullptr;
    }

    Staff* RideGetAssignedMechanic(const Ride& ride)
    {
        if (ride.flags.has(RideFlag::brokenDown))
        {
            if (ride.mechanicStatus == MechanicStatus::heading || ride.mechanicStatus == MechanicStatus::fixing
                || ride.mechanicStatus == MechanicStatus::hasFixedStationBrakes)
            {
                return RideGetMechanic(ride);
            }
        }

        return nullptr;
    }

#pragma endregion

#pragma region Music functions

    /**
     *
     *  Calculates the sample rate for ride music.
     */
    static int32_t RideMusicSampleRate(const Ride& ride)
    {
        int32_t sampleRate = 22050;

        // Alter sample rate for a power cut effect
        if (ride.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown))
        {
            sampleRate = ride.breakdownSoundModifier * 70;
            if (ride.breakdownReasonPending != Breakdown::controlFailure)
                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.flags.hasAny(RideFlag::breakdownPending, RideFlag::brokenDown))
        {
            if (ride.breakdownReasonPending == Breakdown::controlFailure)
            {
                if (!(getGameState().currentTicks & 7))
                    if (ride.breakdownSoundModifier != 255)
                        ride.breakdownSoundModifier++;
            }
            else
            {
                if (ride.flags.has(RideFlag::brokenDown) || ride.breakdownReasonPending == Breakdown::brakesFailure
                    || ride.breakdownReasonPending == Breakdown::controlFailure)
                {
                    if (ride.breakdownSoundModifier != 255)
                        ride.breakdownSoundModifier++;
                }

                if (ride.breakdownSoundModifier == 255)
                {
                    ride.musicTuneId = kTuneIDNull;
                    return true;
                }
            }
        }
        return false;
    }

    /**
     *
     *  Circus music is a sound effect, rather than music. Needs separate processing.
     */
    void CircusMusicUpdate(Ride& ride)
    {
        Vehicle* vehicle = getGameState().entities.getEntity<Vehicle>(ride.vehicles[0]);
        if (vehicle == nullptr || vehicle->status != Vehicle::Status::doingCircusShow)
        {
            ride.musicPosition = 0;
            ride.musicTuneId = kTuneIDNull;
            return;
        }

        if (RideMusicBreakdownEffect(ride))
        {
            return;
        }

        CoordsXYZ rideCoords = ride.getStation().GetStart().ToTileCentre();

        const auto sampleRate = RideMusicSampleRate(ride);

        RideAudio::UpdateMusicInstance(ride, rideCoords, sampleRate);
    }

    /**
     *
     *  rct2: 0x006ABE85
     */
    void DefaultMusicUpdate(Ride& ride)
    {
        if (ride.status != RideStatus::open || !ride.flags.has(RideFlag::music))
        {
            ride.musicTuneId = kTuneIDNull;
            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.musicTuneId == kTuneIDNull)
        {
            auto& objManager = GetContext()->GetObjectManager();
            auto musicObj = objManager.GetLoadedObject<MusicObject>(ride.music);
            if (musicObj != nullptr)
            {
                auto numTracks = musicObj->GetTrackCount();
                ride.musicTuneId = static_cast<uint8_t>(UtilRand() % numTracks);
                ride.musicPosition = 0;
            }
            return;
        }

        CoordsXYZ rideCoords = ride.getStation().GetStart().ToTileCentre();

        int32_t sampleRate = RideMusicSampleRate(ride);

        RideAudio::UpdateMusicInstance(ride, rideCoords, sampleRate);
    }

    static void RideMusicUpdate(Ride& ride)
    {
        const auto& rtd = ride.getRideTypeDescriptor();

        if (!rtd.flags.hasAny(RtdFlag::hasMusicByDefault, RtdFlag::allowMusic))
            return;
        rtd.MusicUpdateFunction(ride);
    }

#pragma endregion

#pragma region Measurement functions

    /**
     *
     *  rct2: 0x006B64F2
     */
    static void RideMeasurementUpdate(Ride& ride, RideMeasurement& measurement)
    {
        if (measurement.vehicle_index >= std::size(ride.vehicles))
            return;

        auto vehicle = getGameState().entities.getEntity<Vehicle>(ride.vehicles[measurement.vehicle_index]);
        if (vehicle == nullptr)
            return;

        if (measurement.flags.has(RideMeasurementFlag::unloading))
        {
            if (vehicle->status != Vehicle::Status::departing && vehicle->status != Vehicle::Status::travellingCableLift)
                return;

            measurement.flags.unset(RideMeasurementFlag::unloading);
            if (measurement.current_station == vehicle->current_station)
                measurement.current_item = 0;
        }

        if (vehicle->status == Vehicle::Status::unloadingPassengers)
        {
            measurement.flags.set(RideMeasurementFlag::unloading);
            return;
        }

        auto trackType = vehicle->GetTrackType();
        if (trackType == TrackElemType::blockBrakes || trackType == TrackElemType::cableLiftHill
            || trackType == TrackElemType::up25ToFlat || trackType == TrackElemType::up60ToFlat
            || trackType == TrackElemType::diagUp25ToFlat || trackType == TrackElemType::diagUp60ToFlat
            || trackType == TrackElemType::diagBlockBrakes)
            if (vehicle->velocity == 0)
                return;

        if (measurement.current_item >= RideMeasurement::kMaxItems)
            return;

        const auto currentTicks = getGameState().currentTicks;

        if (measurement.flags.has(RideMeasurementFlag::gForces))
        {
            auto gForces = vehicle->GetGForces();
            gForces.verticalG = std::clamp(gForces.verticalG / 8, -127, 127);
            gForces.lateralG = std::clamp(gForces.lateralG / 8, -127, 127);

            if (currentTicks & 1)
            {
                gForces.verticalG = (gForces.verticalG + measurement.vertical[measurement.current_item]) / 2;
                gForces.lateralG = (gForces.lateralG + measurement.lateral[measurement.current_item]) / 2;
            }

            measurement.vertical[measurement.current_item] = gForces.verticalG & 0xFF;
            measurement.lateral[measurement.current_item] = gForces.lateralG & 0xFF;
        }

        auto velocity = std::min(std::abs((vehicle->velocity * 5) >> 16), 255);
        auto altitude = std::min(vehicle->z / 8, 255);

        if (currentTicks & 1)
        {
            velocity = (velocity + measurement.velocity[measurement.current_item]) / 2;
            altitude = (altitude + measurement.altitude[measurement.current_item]) / 2;
        }

        measurement.velocity[measurement.current_item] = velocity & 0xFF;
        measurement.altitude[measurement.current_item] = altitude & 0xFF;

        if (currentTicks & 1)
        {
            measurement.current_item++;
            measurement.num_items = std::max(measurement.num_items, measurement.current_item);
        }
    }

    /**
     *
     *  rct2: 0x006B6456
     */
    void RideMeasurementsUpdate()
    {
        PROFILED_FUNCTION();

        if (gLegacyScene == LegacyScene::scenarioEditor)
            return;

        auto& gameState = getGameState();

        // For each ride measurement
        for (auto& ride : RideManager(gameState))
        {
            auto measurement = ride.measurement.get();
            if (measurement != nullptr && ride.flags.has(RideFlag::onTrack) && ride.status != RideStatus::simulating)
            {
                if (measurement->flags.has(RideMeasurementFlag::running))
                {
                    RideMeasurementUpdate(ride, *measurement);
                }
                else
                {
                    // For each vehicle
                    for (int32_t j = 0; j < ride.numTrains; j++)
                    {
                        auto vehicleSpriteIdx = ride.vehicles[j];
                        auto vehicle = gameState.entities.getEntity<Vehicle>(vehicleSpriteIdx);
                        if (vehicle != nullptr)
                        {
                            if (vehicle->status == Vehicle::Status::departing
                                || vehicle->status == Vehicle::Status::travellingCableLift)
                            {
                                measurement->vehicle_index = j;
                                measurement->current_station = vehicle->current_station;
                                measurement->flags.set(RideMeasurementFlag::running);
                                measurement->flags.unset(RideMeasurementFlag::unloading);
                                RideMeasurementUpdate(ride, *measurement);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * If there are more than the threshold of allowed ride measurements, free the non-LRU one.
     */
    static void RideFreeOldMeasurements()
    {
        size_t numRideMeasurements;
        do
        {
            Ride* lruRide{};
            numRideMeasurements = 0;

            auto& gameState = getGameState();
            for (auto& ride : RideManager(gameState))
            {
                if (ride.measurement != nullptr)
                {
                    if (lruRide == nullptr || ride.measurement->last_use_tick > lruRide->measurement->last_use_tick)
                    {
                        lruRide = &ride;
                    }
                    numRideMeasurements++;
                }
            }
            if (numRideMeasurements > kMaxRideMeasurements && lruRide != nullptr)
            {
                lruRide->measurement = {};
                numRideMeasurements--;
            }
        } while (numRideMeasurements > kMaxRideMeasurements);
    }

    std::pair<RideMeasurement*, StringWithArgs> Ride::getMeasurement()
    {
        const auto& rtd = getRideTypeDescriptor();

        // Check if ride type supports data logging
        if (!rtd.flags.has(RtdFlag::hasDataLogging))
        {
            return { nullptr, { STR_DATA_LOGGING_NOT_AVAILABLE_FOR_THIS_TYPE_OF_RIDE, {} } };
        }

        // Check if a measurement already exists for this ride
        if (measurement == nullptr)
        {
            measurement = std::make_unique<RideMeasurement>();
            if (rtd.flags.has(RtdFlag::hasGForces))
            {
                measurement->flags.set(RideMeasurementFlag::gForces);
            }
            RideFreeOldMeasurements();
            assert(measurement != nullptr);
        }

        measurement->last_use_tick = getGameState().currentTicks;
        if (measurement->flags.has(RideMeasurementFlag::running))
        {
            return { measurement.get(), { kStringIdEmpty, {} } };
        }

        auto ft = Formatter();
        ft.Add<StringId>(GetRideComponentName(rtd.NameConvention.vehicle).singular);
        ft.Add<StringId>(GetRideComponentName(rtd.NameConvention.station).singular);
        return { nullptr, { STR_DATA_LOGGING_WILL_START_WHEN_NEXT_LEAVES, ft } };
    }

#pragma endregion

#pragma region Colour functions

    VehicleColour RideGetVehicleColour(const Ride& ride, int32_t vehicleIndex)
    {
        // Prevent indexing array out of bounds
        vehicleIndex = std::clamp<int32_t>(vehicleIndex, 0, std::size(ride.vehicleColours) - 1);
        return ride.vehicleColours[vehicleIndex];
    }

    static bool RideTypeVehicleColourExists(ObjectEntryIndex subType, const VehicleColour& vehicleColour)
    {
        auto& gameState = getGameState();
        for (auto& ride : RideManager(gameState))
        {
            if (ride.subtype != subType)
                continue;
            if (ride.vehicleColours[0].Body != vehicleColour.Body)
                continue;
            return true;
        }
        return false;
    }

    int32_t RideGetUnusedPresetVehicleColour(ObjectEntryIndex subType, uint32_t randomValue)
    {
        const auto* rideEntry = GetRideEntryByIndex(subType);
        if (rideEntry == nullptr)
            return 0;

        const auto* colourPresets = rideEntry->vehicle_preset_list;
        if (colourPresets == nullptr || colourPresets->count == 0)
            return 0;
        if (colourPresets->count == 255)
            return 255;

        // Find all the presets that haven't yet been used in the park for this ride type
        std::vector<uint8_t> unused;
        unused.reserve(colourPresets->count);
        for (uint8_t i = 0; i < colourPresets->count; i++)
        {
            const auto& preset = colourPresets->list[i];
            if (!RideTypeVehicleColourExists(subType, preset))
            {
                unused.push_back(i);
            }
        }

        // If all presets have been used, just go with a random preset
        if (unused.empty())
            return randomValue % colourPresets->count;

        // Choose a random preset from the list of unused presets
        auto unusedIndex = randomValue % unused.size();
        return unused[unusedIndex];
    }

    /**
     *
     *  rct2: 0x006DE52C
     */
    void RideSetVehicleColoursToRandomPreset(Ride& ride, uint8_t preset_index)
    {
        const auto* rideEntry = GetRideEntryByIndex(ride.subtype);
        const auto* presetList = rideEntry->vehicle_preset_list;

        if (presetList->count != 0 && presetList->count != 255)
        {
            assert(preset_index < presetList->count);

            ride.vehicleColourSettings = VehicleColourSettings::same;
            ride.vehicleColours[0] = presetList->list[preset_index];
        }
        else
        {
            ride.vehicleColourSettings = VehicleColourSettings::perTrain;
            for (uint32_t i = 0; i < presetList->count; i++)
            {
                const auto index = i % 32u;
                ride.vehicleColours[i] = presetList->list[index];
            }
        }
    }

#pragma endregion

#pragma region Reachability

    /**
     *
     *  rct2: 0x006B7A5E
     */
    void RideCheckAllReachable()
    {
        auto& gameState = getGameState();
        for (auto& ride : RideManager(gameState))
        {
            if (ride.connectedMessageThrottle != 0)
                ride.connectedMessageThrottle--;

            if (ride.status != RideStatus::open || ride.connectedMessageThrottle != 0)
                continue;

            if (ride.getRideTypeDescriptor().flags.has(RtdFlag::isShopOrFacility))
                RideShopConnected(ride);
            else
                RideEntranceExitConnected(ride);
        }
    }

    /**
     *
     *  rct2: 0x006B7C59
     * @return true if the coordinate is reachable or has no entrance, false otherwise
     */
    static bool RideEntranceExitIsReachable(const TileCoordsXYZD& coordinates)
    {
        if (coordinates.IsNull())
            return true;

        TileCoordsXYZ loc{ coordinates.x, coordinates.y, coordinates.z };
        loc -= TileDirectionDelta[coordinates.direction];

        return MapCoordIsConnected(loc, coordinates.direction);
    }

    static void RideEntranceExitConnected(Ride& ride)
    {
        for (auto& station : ride.getStations())
        {
            auto station_start = station.Start;
            auto entrance = station.Entrance;
            auto exit = station.Exit;

            if (station_start.IsNull())
                continue;

            if (!entrance.IsNull() && !RideEntranceExitIsReachable(entrance))
            {
                // name of ride is parameter of the format string
                Formatter ft;
                ride.formatNameTo(ft);
                if (Config::Get().notifications.rideWarnings)
                {
                    News::AddItemToQueue(News::ItemType::ride, STR_ENTRANCE_NOT_CONNECTED, ride.id.ToUnderlying(), ft);
                }
                ride.connectedMessageThrottle = 3;
            }

            if (!exit.IsNull() && !RideEntranceExitIsReachable(exit))
            {
                // name of ride is parameter of the format string
                Formatter ft;
                ride.formatNameTo(ft);
                if (Config::Get().notifications.rideWarnings)
                {
                    News::AddItemToQueue(News::ItemType::ride, STR_EXIT_NOT_CONNECTED, ride.id.ToUnderlying(), ft);
                }
                ride.connectedMessageThrottle = 3;
            }
        }
    }

    static void RideShopConnected(const Ride& ride)
    {
        auto shopLoc = TileCoordsXY(ride.getStation().Start);
        if (shopLoc.IsNull())
            return;

        TrackElement* trackElement = nullptr;
        TileElement* tileElement = MapGetFirstElementAt(shopLoc);
        do
        {
            if (tileElement == nullptr)
                break;
            if (tileElement->getType() == TileElementType::track && tileElement->asTrack()->getRideIndex() == ride.id)
            {
                trackElement = tileElement->asTrack();
                break;
            }
        } while (!(tileElement++)->isLastForTile());

        if (trackElement == nullptr)
            return;

        auto track_type = trackElement->getTrackType();
        auto ride2 = GetRide(trackElement->getRideIndex());
        if (ride2 == nullptr)
            return;

        const auto& ted = GetTrackElementDescriptor(track_type);
        uint8_t connectionSides = ted.sequenceData.sequences[0].getEntranceConnectionSides();
        uint8_t tile_direction = trackElement->getDirection();
        connectionSides = Numerics::rol4(connectionSides, tile_direction);

        // Now each bit in connectionSides stands for an entrance direction to check
        if (connectionSides == 0)
            return;

        for (auto count = 0; connectionSides != 0; count++)
        {
            if (!(connectionSides & 1))
            {
                connectionSides >>= 1;
                continue;
            }
            connectionSides >>= 1;

            // Flip direction north<->south, east<->west
            uint8_t face_direction = DirectionReverse(count);

            int32_t y2 = shopLoc.y - TileDirectionDelta[face_direction].y;
            int32_t x2 = shopLoc.x - TileDirectionDelta[face_direction].x;

            if (MapCoordIsConnected({ x2, y2, tileElement->baseHeight }, face_direction))
                return;
        }

        // Name of ride is parameter of the format string
        if (Config::Get().notifications.rideWarnings)
        {
            Formatter ft;
            ride2->formatNameTo(ft);
            News::AddItemToQueue(News::ItemType::ride, STR_ENTRANCE_NOT_CONNECTED, ride2->id.ToUnderlying(), ft);
        }

        ride2->connectedMessageThrottle = 3;
    }

#pragma endregion

#pragma region Interface

    static void RideTrackSetMapTooltip(const TrackElement& trackElement)
    {
        auto rideIndex = trackElement.getRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride != nullptr)
        {
            auto ft = Formatter();
            ft.Add<StringId>(STR_RIDE_MAP_TIP);
            ride->formatNameTo(ft);
            ride->formatStatusTo(ft);
            auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
            intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
            ContextBroadcastIntent(&intent);
        }
    }

    static void RideQueueBannerSetMapTooltip(const PathElement& pathElement)
    {
        auto rideIndex = pathElement.getRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return;

        auto ft = Formatter();
        ft.Add<StringId>(STR_RIDE_MAP_TIP);
        ride->formatNameTo(ft);
        ride->formatStatusTo(ft);
        auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
        intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
        ContextBroadcastIntent(&intent);
    }

    static void RideStationSetMapTooltip(const TrackElement& trackElement)
    {
        auto rideIndex = trackElement.getRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return;

        const auto stationIndex = trackElement.getStationIndex();
        const auto stationNumber = ride->getStationNumber(stationIndex);

        auto ft = Formatter();
        ft.Add<StringId>(STR_RIDE_MAP_TIP);
        ft.Add<StringId>(ride->numStations <= 1 ? STR_RIDE_STATION : STR_RIDE_STATION_X);
        ride->formatNameTo(ft);
        ft.Add<StringId>(GetRideComponentName(ride->getRideTypeDescriptor().NameConvention.station).capitalised);
        ft.Add<uint16_t>(stationNumber);
        ride->formatStatusTo(ft);
        auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
        intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
        ContextBroadcastIntent(&intent);
    }

    static void RideEntranceSetMapTooltip(const EntranceElement& entranceElement)
    {
        auto rideIndex = entranceElement.getRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return;

        if (entranceElement.getEntranceType() == EntranceType::rideEntrance)
        {
            // Get the queue length
            int32_t queueLength = 0;
            const auto stationIndex = entranceElement.getStationIndex();
            if (!ride->getStation(stationIndex).Entrance.IsNull())
            {
                queueLength = ride->getStation(stationIndex).QueueLength;
            }

            auto ft = Formatter();
            ft.Add<StringId>(STR_RIDE_MAP_TIP);
            ft.Add<StringId>(ride->numStations <= 1 ? STR_RIDE_ENTRANCE : STR_RIDE_STATION_X_ENTRANCE);
            ride->formatNameTo(ft);

            // String IDs have an extra pop16 for some reason
            ft.Increment(sizeof(uint16_t));

            const auto stationNumber = ride->getStationNumber(stationIndex);
            ft.Add<uint16_t>(stationNumber);

            switch (queueLength)
            {
                case 0:
                    ft.Add<StringId>(STR_QUEUE_EMPTY);
                    break;
                case 1:
                    ft.Add<StringId>(STR_QUEUE_ONE_PERSON);
                    break;
                default:
                    ft.Add<StringId>(STR_QUEUE_PEOPLE);
                    break;
            }
            ft.Add<uint16_t>(queueLength);

            auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
            intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
            ContextBroadcastIntent(&intent);
        }
        else
        {
            auto ft = Formatter();
            ft.Add<StringId>(ride->numStations <= 1 ? STR_RIDE_EXIT : STR_RIDE_STATION_X_EXIT);
            ride->formatNameTo(ft);

            // String IDs have an extra pop16 for some reason
            ft.Increment(sizeof(uint16_t));

            const auto stationIndex = entranceElement.getStationIndex();
            const auto stationNumber = ride->getStationNumber(stationIndex);
            ft.Add<uint16_t>(stationNumber);
            auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
            intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
            ContextBroadcastIntent(&intent);
        }
    }

    void RideSetMapTooltip(const TileElement& tileElement)
    {
        if (tileElement.getType() == TileElementType::entrance)
        {
            RideEntranceSetMapTooltip(*tileElement.asEntrance());
        }
        else if (tileElement.getType() == TileElementType::track)
        {
            const auto* trackElement = tileElement.asTrack();
            if (trackElement->isStation())
            {
                RideStationSetMapTooltip(*trackElement);
            }
            else
            {
                RideTrackSetMapTooltip(*trackElement);
            }
        }
        else if (tileElement.getType() == TileElementType::path)
        {
            RideQueueBannerSetMapTooltip(*tileElement.asPath());
        }
    }

#pragma endregion

    /**
     *
     *  rct2: 0x006B4CC1
     */
    static ResultWithMessage RideModeCheckValidStationNumbers(const Ride& ride)
    {
        uint16_t numStations = 0;
        for (const auto& station : ride.getStations())
        {
            if (!station.Start.IsNull())
            {
                numStations++;
            }
        }

        switch (ride.mode)
        {
            case RideMode::reverseInclineLaunchedShuttle:
            case RideMode::poweredLaunchPassthrough:
            case RideMode::poweredLaunch:
            case RideMode::limPoweredLaunch:
                if (numStations <= 1)
                    return { true };
                return { false, STR_UNABLE_TO_OPERATE_WITH_MORE_THAN_ONE_STATION_IN_THIS_MODE };
            case RideMode::shuttle:
                if (numStations >= 2)
                    return { true };
                return { false, STR_UNABLE_TO_OPERATE_WITH_LESS_THAN_TWO_STATIONS_IN_THIS_MODE };
            default:
            {
                // This is workaround for multiple compilation errors of type "enumeration value ‘RIDE_MODE_*' not handled
                // in switch [-Werror=switch]"
            }
        }

        const auto& rtd = ride.getRideTypeDescriptor();
        if (rtd.flags.has(RtdFlag::hasOneStation) && numStations > 1)
            return { false, STR_UNABLE_TO_OPERATE_WITH_MORE_THAN_ONE_STATION_IN_THIS_MODE };

        return { true };
    }

    /**
     * returns stationIndex of first station on success
     * STATION_INDEX_NULL on failure.
     */
    static StationIndexWithMessage RideModeCheckStationPresent(const Ride& ride)
    {
        auto stationIndex = RideGetFirstValidStationStart(ride);

        if (stationIndex.IsNull())
        {
            const auto& rtd = ride.getRideTypeDescriptor();
            if (!rtd.flags.has(RtdFlag::hasTrack))
                return { StationIndex::GetNull(), STR_NOT_YET_CONSTRUCTED };

            if (rtd.specialType == RtdSpecialType::maze)
                return { StationIndex::GetNull(), STR_NOT_YET_CONSTRUCTED };

            return { StationIndex::GetNull(), STR_REQUIRES_A_STATION_PLATFORM };
        }

        return { stationIndex };
    }

    /**
     *
     *  rct2: 0x006B5872
     */
    static ResultWithMessage RideCheckForEntranceExit(RideId rideIndex)
    {
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return { false };

        if (ride->getRideTypeDescriptor().flags.has(RtdFlag::isShopOrFacility))
            return { true };

        uint8_t entrance = 0;
        uint8_t exit = 0;
        for (const auto& station : ride->getStations())
        {
            if (station.Start.IsNull())
                continue;

            if (!station.Entrance.IsNull())
            {
                entrance = 1;
            }

            if (!station.Exit.IsNull())
            {
                exit = 1;
            }

            // If station start and no entrance/exit
            // Sets same error message as no entrance
            if (station.Exit.IsNull() && station.Entrance.IsNull())
            {
                entrance = 0;
                break;
            }
        }

        if (entrance == 0)
        {
            return { false, STR_ENTRANCE_NOT_YET_BUILT };
        }

        if (exit == 0)
        {
            return { false, STR_EXIT_NOT_YET_BUILT };
        }

        return { true };
    }

    /**
     * Calls FootpathChainRideQueue for all entrances of the ride
     *  rct2: 0x006B5952
     */
    void Ride::chainQueues() const
    {
        for (const auto& station : stations)
        {
            if (station.Entrance.IsNull())
                continue;

            auto mapLocation = station.Entrance.ToCoordsXYZ();

            // This will fire for every entrance on this x, y and z, regardless whether that actually belongs to
            // the ride or not.
            TileElement* tileElement = MapGetFirstElementAt(station.Entrance);
            if (tileElement != nullptr)
            {
                do
                {
                    if (tileElement->getType() != TileElementType::entrance)
                        continue;
                    if (tileElement->getBaseZ() != mapLocation.z)
                        continue;

                    int32_t direction = tileElement->getDirection();
                    FootpathChainRideQueue(
                        id, getStationIndex(&station), mapLocation, tileElement, DirectionReverse(direction));
                } while (!(tileElement++)->isLastForTile());
            }
        }
    }

    /**
     *
     *  rct2: 0x006D3319
     */
    static ResultWithMessage RideCheckBlockBrakes(const CoordsXYE& input, CoordsXYE* output, bool shouldCheckCompleteCircuit)
    {
        if (input.element == nullptr || input.element->getType() != TileElementType::track)
            return { false };

        RideId rideIndex = input.element->asTrack()->getRideIndex();

        auto* windowMgr = Ui::GetWindowManager();
        WindowBase* w = windowMgr->FindByClass(WindowClass::rideConstruction);
        if (w != nullptr && _rideConstructionState != RideConstructionState::state0 && _currentRideIndex == rideIndex)
            RideConstructionInvalidateCurrentTrack();

        TrackCircuitIterator it;
        trackCircuitIteratorBegin(&it, input);
        while (trackCircuitIteratorNext(&it))
        {
            if (trackTypeIsBlockBrakes(it.current.element->asTrack()->getTrackType()))
            {
                auto type = it.last.element->asTrack()->getTrackType();
                if (type == TrackElemType::endStation)
                {
                    *output = it.current;
                    return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_STATION };
                }
                if (trackTypeIsBlockBrakes(type))
                {
                    *output = it.current;
                    return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_EACH_OTHER };
                }
                if (it.last.element->asTrack()->hasChain() && type != TrackElemType::leftCurvedLiftHill
                    && type != TrackElemType::rightCurvedLiftHill)
                {
                    *output = it.current;
                    return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_THE_TOP_OF_THIS_LIFT_HILL };
                }
            }
        }
        if (!it.looped && shouldCheckCompleteCircuit)
        {
            // Not sure why this is the case...
            *output = it.last;
            return { false, STR_BLOCK_BRAKES_CANNOT_BE_USED_DIRECTLY_AFTER_STATION };
        }

        return { true };
    }

    /**
     * Iterates along the track until an inversion (loop, corkscrew, barrel roll etc.) track piece is reached.
     * @param input The start track element and position.
     * @param output The first track element and position which is classified as an inversion.
     * @returns true if an inversion track piece is found, otherwise false.
     *  rct2: 0x006CB149
     */
    static bool RideCheckTrackContainsInversions(const CoordsXYE& input, CoordsXYE* output)
    {
        if (input.element == nullptr)
            return false;

        const auto* trackElement = input.element->asTrack();
        if (trackElement == nullptr)
            return false;

        RideId rideIndex = trackElement->getRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride != nullptr)
        {
            const auto& rtd = ride->getRideTypeDescriptor();
            if (rtd.specialType == RtdSpecialType::maze)
                return true;
        }

        auto* windowMgr = Ui::GetWindowManager();
        WindowBase* w = windowMgr->FindByClass(WindowClass::rideConstruction);
        if (w != nullptr && _rideConstructionState != RideConstructionState::state0 && rideIndex == _currentRideIndex)
        {
            RideConstructionInvalidateCurrentTrack();
        }

        bool moveSlowIt = true;
        TrackCircuitIterator it, slowIt;
        trackCircuitIteratorBegin(&it, input);
        slowIt = it;

        while (trackCircuitIteratorNext(&it))
        {
            auto trackType = it.current.element->asTrack()->getTrackType();
            const auto& ted = GetTrackElementDescriptor(trackType);
            if (ted.flags.has(TrackElementFlag::inversionToNormal))
            {
                *output = it.current;
                return true;
            }

            // Prevents infinite loops
            moveSlowIt = !moveSlowIt;
            if (moveSlowIt)
            {
                trackCircuitIteratorNext(&slowIt);
                if (trackCircuitIteratorsMatch(&it, &slowIt))
                {
                    return false;
                }
            }
        }
        return false;
    }

    /**
     * Iterates along the track until a banked track piece is reached.
     * @param input The start track element and position.
     * @param output The first track element and position which is banked.
     * @returns true if a banked track piece is found, otherwise false.
     *  rct2: 0x006CB1D3
     */
    static bool RideCheckTrackContainsBanked(const CoordsXYE& input, CoordsXYE* output)
    {
        if (input.element == nullptr)
            return false;

        const auto* trackElement = input.element->asTrack();
        if (trackElement == nullptr)
            return false;

        auto rideIndex = trackElement->getRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return false;

        const auto& rtd = ride->getRideTypeDescriptor();
        if (rtd.specialType == RtdSpecialType::maze)
            return true;

        auto* windowMgr = Ui::GetWindowManager();
        WindowBase* w = windowMgr->FindByClass(WindowClass::rideConstruction);
        if (w != nullptr && _rideConstructionState != RideConstructionState::state0 && rideIndex == _currentRideIndex)
        {
            RideConstructionInvalidateCurrentTrack();
        }

        bool moveSlowIt = true;
        TrackCircuitIterator it, slowIt;
        trackCircuitIteratorBegin(&it, input);
        slowIt = it;

        while (trackCircuitIteratorNext(&it))
        {
            auto trackType = it.current.element->asTrack()->getTrackType();
            const auto& ted = GetTrackElementDescriptor(trackType);
            if (ted.flags.has(TrackElementFlag::banked))
            {
                *output = it.current;
                return true;
            }

            // Prevents infinite loops
            moveSlowIt = !moveSlowIt;
            if (moveSlowIt)
            {
                trackCircuitIteratorNext(&slowIt);
                if (trackCircuitIteratorsMatch(&it, &slowIt))
                {
                    return false;
                }
            }
        }
        return false;
    }

    /**
     *
     *  rct2: 0x006CB25D
     */
    static int32_t RideCheckStationLength(const CoordsXYE& input, CoordsXYE* output)
    {
        auto* windowMgr = Ui::GetWindowManager();
        WindowBase* w = windowMgr->FindByClass(WindowClass::rideConstruction);
        if (w != nullptr && _rideConstructionState != RideConstructionState::state0
            && _currentRideIndex == input.element->asTrack()->getRideIndex())
        {
            RideConstructionInvalidateCurrentTrack();
        }

        output->x = input.x;
        output->y = input.y;
        output->element = input.element;
        TrackBeginEnd trackBeginEnd;
        while (trackBlockGetPrevious(*output, &trackBeginEnd))
        {
            output->x = trackBeginEnd.begin_x;
            output->y = trackBeginEnd.begin_y;
            output->element = trackBeginEnd.begin_element;
        }

        int32_t num_station_elements = 0;
        CoordsXYE last_good_station = *output;

        do
        {
            const auto& ted = GetTrackElementDescriptor(output->element->asTrack()->getTrackType());
            if (ted.sequenceData.sequences[0].flags.has(SequenceFlag::trackOrigin))
            {
                num_station_elements++;
                last_good_station = *output;
            }
            else
            {
                if (num_station_elements == 0)
                    continue;
                if (num_station_elements == 1)
                {
                    return 0;
                }
                num_station_elements = 0;
            }
        } while (trackBlockGetNext(output, output, nullptr, nullptr));

        // Prevent returning a pointer to a map element with no track.
        *output = last_good_station;
        if (num_station_elements == 1)
            return 0;

        return 1;
    }

    /**
     *
     *  rct2: 0x006CB2DA
     */
    static bool RideCheckStartAndEndIsStation(const CoordsXYE& input)
    {
        CoordsXYE trackBack, trackFront;

        RideId rideIndex = input.element->asTrack()->getRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return false;

        auto* windowMgr = Ui::GetWindowManager();
        auto w = windowMgr->FindByClass(WindowClass::rideConstruction);
        if (w != nullptr && _rideConstructionState != RideConstructionState::state0 && rideIndex == _currentRideIndex)
        {
            RideConstructionInvalidateCurrentTrack();
        }

        // Check back of the track
        trackGetBack(input, &trackBack);
        auto trackType = trackBack.element->asTrack()->getTrackType();
        const auto& tedBack = GetTrackElementDescriptor(trackType);
        if (!tedBack.sequenceData.sequences[0].flags.has(SequenceFlag::trackOrigin))
        {
            return false;
        }
        ride->chairliftBullwheelLocation[0] = TileCoordsXYZ{ CoordsXYZ{ trackBack.x, trackBack.y,
                                                                        trackBack.element->getBaseZ() } };

        // Check front of the track
        trackGetFront(input, &trackFront);
        trackType = trackFront.element->asTrack()->getTrackType();
        const auto& tedFront = GetTrackElementDescriptor(trackType);
        if (!tedFront.sequenceData.sequences[0].flags.has(SequenceFlag::trackOrigin))
        {
            return false;
        }
        ride->chairliftBullwheelLocation[1] = TileCoordsXYZ{ CoordsXYZ{ trackFront.x, trackFront.y,
                                                                        trackFront.element->getBaseZ() } };
        return true;
    }

    /**
     * Sets the position and direction of the returning point on the track of a boat hire ride. This will either be the end of
     * the station or the last track piece from the end of the direction. rct2: 0x006B4D39
     */
    static void RideSetBoatHireReturnPoint(Ride& ride, const CoordsXYE& startElement)
    {
        auto trackType = TrackElemType::none;
        auto returnPos = startElement;
        int32_t startX = returnPos.x;
        int32_t startY = returnPos.y;
        TrackBeginEnd trackBeginEnd;
        while (trackBlockGetPrevious(returnPos, &trackBeginEnd))
        {
            // If previous track is back to the starting x, y, then break loop (otherwise possible infinite loop)
            if (trackType != TrackElemType::none && startX == trackBeginEnd.begin_x && startY == trackBeginEnd.begin_y)
                break;

            auto trackCoords = CoordsXYZ{ trackBeginEnd.begin_x, trackBeginEnd.begin_y, trackBeginEnd.begin_z };
            int32_t direction = trackBeginEnd.begin_direction;
            trackType = trackBeginEnd.begin_element->asTrack()->getTrackType();
            auto newCoords = GetTrackElementOriginAndApplyChanges(
                { trackCoords, static_cast<Direction>(direction) }, trackType, 0, &returnPos.element, {});
            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.rotationBegin;
        ride.boatHireReturnDirection = returnPos.element->getDirectionWithOffset(elementReturnDirection);
        ride.boatHireReturnPosition = TileCoordsXY{ returnPos };
    }

    /**
     *
     *  rct2: 0x006B4D39
     */
    static void RideSetMazeEntranceExitPoints(Ride& ride)
    {
        // Needs room for an entrance and an exit per station, plus one position for the list terminator.
        TileCoordsXYZD positions[(Limits::kMaxStationsPerRide * 2) + 1];

        // Create a list of all the entrance and exit positions
        TileCoordsXYZD* position = positions;
        for (const auto& station : ride.getStations())
        {
            if (!station.Entrance.IsNull())
            {
                *position++ = station.Entrance;
            }
            if (!station.Exit.IsNull())
            {
                *position++ = station.Exit;
            }
        }
        position->SetNull();

        // Enumerate entrance and exit positions
        for (position = positions; !position->IsNull(); position++)
        {
            auto entranceExitMapPos = position->ToCoordsXYZ();

            TileElement* tileElement = MapGetFirstElementAt(*position);
            do
            {
                if (tileElement == nullptr)
                    break;
                if (tileElement->getType() != TileElementType::entrance)
                    continue;
                if (tileElement->asEntrance()->getEntranceType() != EntranceType::rideEntrance
                    && tileElement->asEntrance()->getEntranceType() != EntranceType::rideExit)
                {
                    continue;
                }
                if (tileElement->getBaseZ() != entranceExitMapPos.z)
                    continue;

                MazeEntranceHedgeRemoval({ entranceExitMapPos, tileElement });
            } while (!(tileElement++)->isLastForTile());
        }
    }

    void SetBrakeClosedMultiTile(TrackElement& trackElement, const CoordsXY& trackLocation, bool isClosed)
    {
        switch (trackElement.getTrackType())
        {
            case TrackElemType::diagUp25ToFlat:
            case TrackElemType::diagUp60ToFlat:
            case TrackElemType::cableLiftHill:
            case TrackElemType::diagBrakes:
            case TrackElemType::diagBlockBrakes:
                GetTrackElementOriginAndApplyChanges(
                    { trackLocation, trackElement.getBaseZ(), trackElement.getDirection() }, trackElement.getTrackType(),
                    isClosed, nullptr, { TrackElementSetFlag::brakeClosed });
                break;
            default:
                trackElement.setBrakeClosed(isClosed);
        }
    }

    /**
     * Opens all block brakes of a ride.
     *  rct2: 0x006B4E6B
     */
    static void RideOpenBlockBrakes(const CoordsXYE& startElement)
    {
        CoordsXYE currentElement = startElement;
        do
        {
            auto trackType = currentElement.element->asTrack()->getTrackType();
            switch (trackType)
            {
                case TrackElemType::blockBrakes:
                case TrackElemType::diagBlockBrakes:
                    BlockBrakeSetLinkedBrakesClosed(
                        CoordsXYZ(currentElement.x, currentElement.y, currentElement.element->getBaseZ()),
                        *currentElement.element->asTrack(), false);
                    [[fallthrough]];
                case TrackElemType::diagUp25ToFlat:
                case TrackElemType::diagUp60ToFlat:
                case TrackElemType::cableLiftHill:
                case TrackElemType::endStation:
                case TrackElemType::up25ToFlat:
                case TrackElemType::up60ToFlat:
                    SetBrakeClosedMultiTile(*currentElement.element->asTrack(), { currentElement.x, currentElement.y }, false);
                    break;
                default:
                    break;
            }
        } while (trackBlockGetNext(&currentElement, &currentElement, nullptr, nullptr)
                 && currentElement.element != startElement.element);
    }

    /**
     * Set the open status of brakes adjacent to the block brake
     */
    void BlockBrakeSetLinkedBrakesClosed(const CoordsXYZ& vehicleTrackLocation, TrackElement& trackElement, bool isClosed)
    {
        uint8_t brakeSpeed = trackElement.getBrakeBoosterSpeed();

        auto tileElement = reinterpret_cast<TileElement*>(&trackElement);
        auto location = vehicleTrackLocation;
        TrackBeginEnd trackBeginEnd, slowTrackBeginEnd;
        TileElement slowTileElement = *tileElement;
        bool counter = true;
        CoordsXY slowLocation = location;
        do
        {
            if (!trackBlockGetPrevious({ location, tileElement }, &trackBeginEnd))
            {
                return;
            }
            if (trackBeginEnd.begin_x == vehicleTrackLocation.x && trackBeginEnd.begin_y == vehicleTrackLocation.y
                && tileElement == trackBeginEnd.begin_element)
            {
                return;
            }

            location.x = trackBeginEnd.end_x;
            location.y = trackBeginEnd.end_y;
            location.z = trackBeginEnd.begin_z;
            tileElement = trackBeginEnd.begin_element;

            if (trackTypeIsBrakes(tileElement->asTrack()->getTrackType()))
            {
                SetBrakeClosedMultiTile(
                    *tileElement->asTrack(), { trackBeginEnd.begin_x, trackBeginEnd.begin_y },
                    (tileElement->asTrack()->getBrakeBoosterSpeed() >= brakeSpeed) || isClosed);
            }

            // prevent infinite loop
            counter = !counter;
            if (counter)
            {
                trackBlockGetPrevious({ slowLocation, &slowTileElement }, &slowTrackBeginEnd);
                slowLocation.x = slowTrackBeginEnd.end_x;
                slowLocation.y = slowTrackBeginEnd.end_y;
                slowTileElement = *(slowTrackBeginEnd.begin_element);
                if (slowLocation == location && slowTileElement.getBaseZ() == tileElement->getBaseZ()
                    && slowTileElement.getType() == tileElement->getType()
                    && slowTileElement.getDirection() == tileElement->getDirection())
                {
                    return;
                }
            }
        } while (trackTypeIsBrakes(trackBeginEnd.begin_element->asTrack()->getTrackType()));
    }

    /**
     *
     *  rct2: 0x006B4D26
     */
    static void RideSetStartFinishPoints(RideId rideIndex, const CoordsXYE& startElement)
    {
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return;

        const auto& rtd = ride->getRideTypeDescriptor();
        if (rtd.specialType == RtdSpecialType::maze)
            RideSetMazeEntranceExitPoints(*ride);
        else if (rtd.specialType == RtdSpecialType::boatHire)
            RideSetBoatHireReturnPoint(*ride, startElement);

        if (ride->isBlockSectioned() && !ride->flags.has(RideFlag::onTrack))
        {
            RideOpenBlockBrakes(startElement);
        }
    }

    /**
     *
     *  rct2: 0x0069ED9E
     */
    static int32_t count_free_misc_sprite_slots()
    {
        auto& gameState = getGameState();
        int32_t miscSpriteCount = gameState.entities.getMiscEntityCount();
        int32_t remainingSpriteCount = gameState.entities.getNumFreeEntities();
        return std::max(0, miscSpriteCount + remainingSpriteCount - 300);
    }

    static constexpr CoordsXY word_9A3AB4[4] = {
        { 0, 0 },
        { 0, -96 },
        { -96, -96 },
        { -96, 0 },
    };

    // clang-format off
    static constexpr CoordsXY word_9A2A60[] = {
        { 0, 16 },
        { 16, 31 },
        { 31, 16 },
        { 16, 0 },
        { 16, 16 },
        { 64, 64 },
        { 64, -32 },
        { -32, -32 },
        { -32, 64 },
    };
    // clang-format on

    /**
     *
     *  rct2: 0x006DD90D
     */
    static Vehicle* VehicleCreateCar(
        Ride& ride, int32_t carEntryIndex, int32_t carIndex, int32_t vehicleIndex, const CoordsXYZ& carPosition,
        int32_t* remainingDistance, TrackElement* trackElement)
    {
        if (trackElement == nullptr)
            return nullptr;

        auto rideEntry = ride.getRideEntry();
        if (rideEntry == nullptr)
            return nullptr;

        auto& carEntry = rideEntry->Cars[carEntryIndex];

        auto* vehicle = getGameState().entities.createEntity<Vehicle>();
        if (vehicle == nullptr)
            return nullptr;

        vehicle->ride = ride.id;
        vehicle->ride_subtype = ride.subtype;

        vehicle->vehicle_type = carEntryIndex;
        vehicle->SubType = carIndex == 0 ? Vehicle::Type::head : Vehicle::Type::tail;
        vehicle->var_44 = Numerics::ror32(carEntry.spacing, 10) & 0xFFFF;

        const auto halfSpacing = carEntry.spacing >> 1;
        *remainingDistance -= halfSpacing;
        vehicle->remaining_distance = *remainingDistance;

        if (!carEntry.flags.has(CarEntryFlag::isGoKart))
        {
            *remainingDistance -= halfSpacing;
        }

        // Loc6DD9A5:
        vehicle->spriteData.width = carEntry.spriteWidth;
        vehicle->spriteData.heightMin = carEntry.spriteHeightNegative;
        vehicle->spriteData.heightMax = carEntry.spriteHeightPositive;
        vehicle->mass = carEntry.carMass;
        vehicle->num_seats = carEntry.numSeats;
        vehicle->speed = carEntry.poweredMaxSpeed;
        vehicle->powered_acceleration = carEntry.poweredAcceleration;
        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 = Audio::SoundId::null;
        vehicle->sound2_id = Audio::SoundId::null;
        vehicle->next_vehicle_on_train = EntityId::GetNull();
        vehicle->CollisionDetectionTimer = 0;
        vehicle->animation_frame = 0;
        vehicle->animationState = 0;
        vehicle->scream_sound_id = Audio::SoundId::null;
        vehicle->pitch = VehiclePitch::flat;
        vehicle->roll = VehicleRoll::unbanked;
        vehicle->target_seat_rotation = 4;
        vehicle->seat_rotation = 4;
        for (size_t i = 0; i < std::size(vehicle->peep); i++)
        {
            vehicle->peep[i] = EntityId::GetNull();
        }

        const auto& rtd = ride.getRideTypeDescriptor();
        if (carEntry.flags.has(CarEntryFlag::useDodgemCarPlacement))
        {
            // Loc6DDCA4:
            vehicle->TrackSubposition = VehicleTrackSubposition::standard;
            int32_t direction = trackElement->getDirection();
            auto dodgemPos = carPosition + CoordsXYZ{ word_9A3AB4[direction], 0 };
            vehicle->TrackLocation = dodgemPos;
            vehicle->current_station = trackElement->getStationIndex();

            dodgemPos.z += rtd.Heights.VehicleZOffset;

            vehicle->SetTrackDirection(0);
            vehicle->SetTrackType(trackElement->getTrackType());
            vehicle->track_progress = 0;
            vehicle->SetState(Vehicle::Status::movingToEndOfStation);
            vehicle->flags.clearAll();

            CoordsXY chosenLoc;
            auto numAttempts = 0;
            // Loc6DDD26:
            do
            {
                numAttempts++;
                // This can happen when trying to spawn dozens of cars in a tiny area.
                if (numAttempts > 10000)
                    return nullptr;

                vehicle->orientation = ScenarioRand() & 0x1E;
                chosenLoc.y = dodgemPos.y + (ScenarioRand() & 0xFF);
                chosenLoc.x = dodgemPos.x + (ScenarioRand() & 0xFF);
            } while (vehicle->DodgemsCarWouldCollideAt(chosenLoc).has_value());

            vehicle->moveToAndUpdateSpatialIndex({ chosenLoc, dodgemPos.z });
        }
        else
        {
            VehicleTrackSubposition subposition = VehicleTrackSubposition::standard;
            if (carEntry.flags.has(CarEntryFlag::isChairlift))
            {
                subposition = VehicleTrackSubposition::chairliftGoingOut;
            }

            if (carEntry.flags.has(CarEntryFlag::isGoKart))
            {
                // Choose which lane Go Kart should start in
                subposition = VehicleTrackSubposition::goKartsLeftLane;
                if (vehicleIndex & 1)
                {
                    subposition = VehicleTrackSubposition::goKartsRightLane;
                }
            }
            if (carEntry.flags.has(CarEntryFlag::isMiniGolf))
            {
                subposition = VehicleTrackSubposition::miniGolfStart9;
                vehicle->var_D3 = 0;
                vehicle->mini_golf_current_animation = MiniGolfAnimation::walk;
                vehicle->miniGolfFlags.clearAll();
            }
            if (carEntry.flags.has(CarEntryFlag::isReverserCoasterBogie))
            {
                if (vehicle->IsHead())
                {
                    subposition = VehicleTrackSubposition::reverserRCFrontBogie;
                }
            }
            if (carEntry.flags.has(CarEntryFlag::isReverserCoasterPassengerCar))
            {
                subposition = VehicleTrackSubposition::reverserRCRearBogie;
            }
            vehicle->TrackSubposition = subposition;

            auto chosenLoc = carPosition;
            vehicle->TrackLocation = chosenLoc;

            int32_t direction = trackElement->getDirection();
            vehicle->orientation = direction << 3;

            if (ride.getRideTypeDescriptor().specialType == RtdSpecialType::spaceRings)
            {
                direction = 4;
            }
            else
            {
                if (rtd.flags.has(RtdFlag::vehicleIsIntegral))
                {
                    if (rtd.StartTrackPiece != TrackElemType::flatTrack1x4B)
                    {
                        if (rtd.StartTrackPiece != TrackElemType::flatTrack1x4A)
                        {
                            if (ride.getRideTypeDescriptor().specialType == RtdSpecialType::enterprise)
                            {
                                direction += 5;
                            }
                            else
                            {
                                direction = 4;
                            }
                        }
                    }
                }
            }

            chosenLoc += CoordsXYZ{ word_9A2A60[direction], rtd.Heights.VehicleZOffset };

            vehicle->current_station = trackElement->getStationIndex();

            vehicle->moveTo(chosenLoc);
            vehicle->SetTrackType(trackElement->getTrackType());
            vehicle->SetTrackDirection(vehicle->orientation >> 3);
            vehicle->track_progress = 31;
            if (carEntry.flags.has(CarEntryFlag::isMiniGolf))
            {
                vehicle->track_progress = 15;
            }
            vehicle->flags = { VehicleFlag::collisionDisabled };
            if (carEntry.flags.has(CarEntryFlag::hasInvertedSpriteSet))
            {
                if (trackElement->isInverted())
                {
                    vehicle->flags.set(VehicleFlag::carIsInverted);
                }
            }
            vehicle->SetState(Vehicle::Status::movingToEndOfStation);

            if (ride.flags.has(RideFlag::reversedTrains))
            {
                vehicle->SubType = carIndex == (ride.numCarsPerTrain - 1) ? Vehicle::Type::head : Vehicle::Type::tail;
                vehicle->flags.set(VehicleFlag::carIsReversed);
            }
        }

        // Loc6DDD5E:
        vehicle->num_peeps = 0;
        vehicle->next_free_seat = 0;
        vehicle->BoatLocation.SetNull();
        return vehicle;
    }

    /**
     *
     *  rct2: 0x006DD84C
     */
    static TrainReference VehicleCreateTrain(
        Ride& ride, const CoordsXYZ& trainPos, int32_t vehicleIndex, int32_t* remainingDistance, TrackElement* trackElement)
    {
        TrainReference train = { nullptr, nullptr };
        bool isReversed = ride.flags.has(RideFlag::reversedTrains);

        for (int32_t carIndex = 0; carIndex < ride.numCarsPerTrain; carIndex++)
        {
            auto carSpawnIndex = (isReversed) ? (ride.numCarsPerTrain - 1) - carIndex : carIndex;

            auto vehicle = RideEntryGetVehicleAtPosition(ride.subtype, ride.numCarsPerTrain, carSpawnIndex);
            auto car = VehicleCreateCar(ride, vehicle, carSpawnIndex, vehicleIndex, trainPos, remainingDistance, trackElement);
            if (car == nullptr)
                break;

            if (carIndex == 0)
            {
                train.head = car;
            }
            else
            {
                // Link the previous car with this car
                train.tail->next_vehicle_on_train = car->id;
                train.tail->next_vehicle_on_ride = car->id;
                car->prev_vehicle_on_ride = train.tail->id;
            }
            train.tail = car;
        }

        return train;
    }

    static bool VehicleCreateTrains(Ride& ride, const CoordsXYZ& trainsPos, TrackElement* trackElement, int16_t numberOfTrains)
    {
        TrainReference firstTrain = {};
        TrainReference lastTrain = {};
        int32_t remainingDistance = 0;
        bool allTrainsCreated = true;

        for (int32_t vehicleIndex = 0; vehicleIndex < numberOfTrains; vehicleIndex++)
        {
            if (ride.isBlockSectioned())
            {
                remainingDistance = 0;
            }
            TrainReference train = VehicleCreateTrain(ride, trainsPos, vehicleIndex, &remainingDistance, trackElement);
            if (train.head == nullptr || train.tail == nullptr)
            {
                allTrainsCreated = false;
                continue;
            }

            if (vehicleIndex == 0)
            {
                firstTrain = train;
            }
            else
            {
                // Link the end of the previous train with the front of this train
                lastTrain.tail->next_vehicle_on_ride = train.head->id;
                train.head->prev_vehicle_on_ride = lastTrain.tail->id;
            }
            lastTrain = train;

            for (int32_t i = 0; i <= Limits::kMaxTrainsPerRide; i++)
            {
                if (ride.vehicles[i].IsNull())
                {
                    ride.vehicles[i] = train.head->id;
                    break;
                }
            }
        }

        // Link the first train and last train together. Nullptr checks are there to keep Clang happy.
        if (lastTrain.tail != nullptr)
            firstTrain.head->prev_vehicle_on_ride = lastTrain.tail->id;
        if (firstTrain.head != nullptr)
            lastTrain.tail->next_vehicle_on_ride = firstTrain.head->id;

        return allTrainsCreated;
    }

    /**
     *
     *  rct2: 0x006DDE9E
     */
    static void RidecreateVehiclesFindFirstBlock(const Ride& ride, CoordsXYE* outXYElement)
    {
        Vehicle* vehicle = getGameState().entities.getEntity<Vehicle>(ride.vehicles[0]);
        if (vehicle == nullptr)
            return;

        auto curTrackPos = vehicle->TrackLocation;
        auto curTrackElement = MapGetTrackElementAt(curTrackPos);

        assert(curTrackElement != nullptr);

        CoordsXY trackPos = curTrackPos;
        auto trackElement = curTrackElement;
        TrackBeginEnd trackBeginEnd;
        while (trackBlockGetPrevious({ trackPos, reinterpret_cast<TileElement*>(trackElement) }, &trackBeginEnd))
        {
            trackPos = { trackBeginEnd.end_x, trackBeginEnd.end_y };
            trackElement = trackBeginEnd.begin_element->asTrack();
            if (trackPos == curTrackPos && trackElement == curTrackElement)
            {
                break;
            }

            auto trackType = trackElement->getTrackType();
            switch (trackType)
            {
                case TrackElemType::diagUp25ToFlat:
                case TrackElemType::diagUp60ToFlat:
                    if (!trackElement->hasChain())
                    {
                        break;
                    }
                    [[fallthrough]];
                case TrackElemType::diagBlockBrakes:
                {
                    TileElement* tileElement = MapGetTrackElementAtOfTypeSeq(
                        { trackBeginEnd.begin_x, trackBeginEnd.begin_y, trackBeginEnd.begin_z }, trackType, 0);

                    if (tileElement != nullptr)
                    {
                        outXYElement->x = trackBeginEnd.begin_x;
                        outXYElement->y = trackBeginEnd.begin_y;
                        outXYElement->element = tileElement;
                        return;
                    }
                    break;
                }
                case TrackElemType::up25ToFlat:
                case TrackElemType::up60ToFlat:
                    if (!trackElement->hasChain())
                    {
                        break;
                    }
                    [[fallthrough]];
                case TrackElemType::endStation:
                case TrackElemType::blockBrakes:
                    *outXYElement = { trackPos, reinterpret_cast<TileElement*>(trackElement) };
                    return;
                default:
                    break;
            }
        }

        outXYElement->x = curTrackPos.x;
        outXYElement->y = curTrackPos.y;
        outXYElement->element = reinterpret_cast<TileElement*>(curTrackElement);
    }

    /**
     * Create and place the rides vehicles
     *  rct2: 0x006DD84C
     */
    ResultWithMessage Ride::createVehicles(const CoordsXYE& element, bool isApplying, bool isSimulating)
    {
        updateMaxVehicles();
        if (subtype == kObjectEntryIndexNull)
        {
            return { true };
        }

        // Check if there are enough free sprite slots for all the vehicles
        int32_t numberOfTrains = numTrains;
        if (isBlockSectioned() && isSimulating)
        {
            numberOfTrains = 1;
        }
        int32_t totalCars = numberOfTrains * numCarsPerTrain;
        if (totalCars > count_free_misc_sprite_slots())
        {
            return { false, STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES };
        }

        if (!isApplying)
        {
            return { true };
        }

        auto* trackElement = element.element->asTrack();
        auto vehiclePos = CoordsXYZ{ element, element.element->getBaseZ() };
        int32_t direction = trackElement->getDirection();

        //
        if (mode == RideMode::stationToStation)
        {
            vehiclePos -= CoordsXYZ{ CoordsDirectionDelta[direction], 0 };

            trackElement = MapGetTrackElementAt(vehiclePos);

            vehiclePos.z = trackElement->getBaseZ();
        }

        if (!VehicleCreateTrains(*this, vehiclePos, trackElement, numberOfTrains))
        {
            // This flag is needed for Ride::removeVehicles()
            flags.set(RideFlag::onTrack);
            removeVehicles();
            return { false, STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES };
        }
        // return true;

        // Initialise station departs
        // 006DDDD0:
        flags.set(RideFlag::onTrack);
        for (int32_t i = 0; i < Limits::kMaxStationsPerRide; i++)
        {
            stations[i].Depart = (stations[i].Depart & kStationDepartFlag) | 1;
        }

        const auto& rtd = getRideTypeDescriptor();
        if (rtd.specialType != RtdSpecialType::spaceRings && !rtd.flags.has(RtdFlag::vehicleIsIntegral))
        {
            if (isBlockSectioned() && !isSimulating)
            {
                CoordsXYE firstBlock{};
                RidecreateVehiclesFindFirstBlock(*this, &firstBlock);
                moveTrainsToBlockBrakes(
                    { firstBlock.x, firstBlock.y, firstBlock.element->getBaseZ() }, *firstBlock.element->asTrack());
            }
            else
            {
                for (int32_t i = 0; i < numTrains; i++)
                {
                    Vehicle* vehicle = getGameState().entities.getEntity<Vehicle>(vehicles[i]);
                    if (vehicle == nullptr)
                    {
                        continue;
                    }

                    auto carEntry = vehicle->Entry();

                    if (!carEntry->flags.has(CarEntryFlag::useDodgemCarPlacement))
                    {
                        vehicle->updateTrackMotionTrain(nullptr);
                    }

                    vehicle->EnableCollisionsForTrain();
                }
            }
        }
        RideUpdateVehicleColours(*this);
        return { true };
    }

    /**
     * Move all the trains so each one will be placed at the block brake of a different block.
     * The first vehicle will placed into the first block and all other vehicles in the blocks
     * preceding that block.
     *  rct2: 0x006DDF9C
     */
    void Ride::moveTrainsToBlockBrakes(const CoordsXYZ& firstBlockPosition, TrackElement& firstBlock)
    {
        // If the ride has a cable lift, we don't want to fetch the cable lift element and the block preceding it
        TrackElement* cableLiftTileElement = nullptr;
        TrackElement* cableLiftPreviousBlock = nullptr;
        if (flags.has(RideFlag::cableLiftHillComponentUsed))
        {
            cableLiftTileElement = MapGetTrackElementAt(cableLiftLoc);
            if (cableLiftTileElement != nullptr)
            {
                CoordsXYZ location = cableLiftLoc;
                cableLiftPreviousBlock = trackGetPreviousBlock(location, reinterpret_cast<TileElement*>(cableLiftTileElement));
            }
        }

        for (int32_t i = 0; i < numTrains; i++)
        {
            auto train = getGameState().entities.getEntity<Vehicle>(vehicles[i]);
            if (train == nullptr)
                continue;

            // At this point, all vehicles have state of MovingToEndOfStation, which slowly moves forward at a constant speed
            // regardless of incline. The first vehicle stops at the station immediately, while all other vehicles seek forward
            // until they reach a closed block brake. The block brake directly before the station is set to closed every frame
            // because the trains will open the block brake when the tail leaves the station. Brakes have no effect at this
            // time, so do not set linked brakes when closing the first block.
            train->updateTrackMotionTrain(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;
                }

                // Setting the first block before the cable lift to the same state as the cable lift ensures that any train
                // which would be placed on the cable lift will instead stop on the block before it. As there can only be one
                // cable lift per ride and there must always be at least one block left free, there will be enough blocks
                // remaining. This fixes the bug in #1122.
                if (cableLiftTileElement != nullptr && cableLiftPreviousBlock != nullptr)
                {
                    cableLiftPreviousBlock->setBrakeClosed(cableLiftTileElement->isBrakeClosed());
                }
                firstBlock.setBrakeClosed(true);
                for (Vehicle* car = train; car != nullptr;
                     car = getGameState().entities.getEntity<Vehicle>(car->next_vehicle_on_train))
                {
                    car->velocity = 0;
                    car->acceleration = 0;
                    car->SwingSprite = 0;
                    car->remaining_distance += 13962;
                }
            } while (!(train->updateTrackMotionTrain(nullptr) & VEHICLE_UPDATE_MOTION_TRACK_FLAG_VEHICLE_AT_BLOCK_BRAKE));

            // All vehicles are in position, set the block brake directly before the station one last time and make sure the
            // brakes are set appropriately
            SetBrakeClosedMultiTile(firstBlock, firstBlockPosition, true);
            if (trackTypeIsBlockBrakes(firstBlock.getTrackType()))
            {
                BlockBrakeSetLinkedBrakesClosed(firstBlockPosition, firstBlock, true);
            }
            for (Vehicle* car = train; car != nullptr;
                 car = getGameState().entities.getEntity<Vehicle>(car->next_vehicle_on_train))
            {
                car->flags.unset(VehicleFlag::collisionDisabled);
                car->SetState(Vehicle::Status::travelling, car->sub_state);
                if ((car->GetTrackType()) == TrackElemType::endStation)
                {
                    car->SetState(Vehicle::Status::movingToEndOfStation, car->sub_state);
                }
            }
        }

        // After all trains are in position, set the block preceding the cable lift to open.
        if (cableLiftPreviousBlock != nullptr)
        {
            cableLiftPreviousBlock->setBrakeClosed(false);
        }
    }

    static bool RideGetStationTile(const Ride& ride, CoordsXYE* output)
    {
        for (const auto& station : ride.getStations())
        {
            CoordsXYZ trackStart = station.GetStart();
            if (trackStart.IsNull())
                continue;

            TileElement* tileElement = MapGetTrackElementAtOfType(trackStart, TrackElemType::endStation);
            if (tileElement == nullptr)
                continue;

            *output = { trackStart.x, trackStart.y, tileElement };
            return true;
        }
        return false;
    }

    /**
     * Checks and initialises the cable lift track returns false if unable to find
     * appropriate track.
     *  rct2: 0x006D31A6
     */
    static ResultWithMessage RideInitialiseCableLiftTrack(const Ride& ride, bool isApplying)
    {
        // Despawn existing cable lift tiles
        CoordsXYE stationTile;
        if (!RideGetStationTile(ride, &stationTile))
            return { false, STR_CABLE_LIFT_HILL_MUST_START_IMMEDIATELY_AFTER_STATION_OR_BLOCK_BRAKE };

        if (isApplying)
        {
            // In case circuit is incomplete, find the start of the track in order to ensure all tiles connected
            // to the station are cleared
            RideGetStartOfTrack(&stationTile);

            TrackCircuitIterator it;
            trackCircuitIteratorBegin(&it, stationTile);
            while (trackCircuitIteratorNext(&it))
            {
                TileElement* tileElement = it.current.element;
                GetTrackElementOriginAndApplyChanges(
                    { { it.current, tileElement->getBaseZ() }, tileElement->getDirection() },
                    tileElement->asTrack()->getTrackType(), 0, &tileElement, { TrackElementSetFlag::cableLiftOff });
            }
        }

        // Spawn new cable lift tiles
        auto cableLiftTileElement = MapGetTrackElementAt(ride.cableLiftLoc);
        CoordsXYE cableLiftCoords = { ride.cableLiftLoc, reinterpret_cast<TileElement*>(cableLiftTileElement) };
        if (cableLiftTileElement == nullptr)
            return { false };

        TrackCircuitIterator it;
        trackCircuitIteratorBegin(&it, cableLiftCoords);
        while (trackCircuitIteratorPrevious(&it))
        {
            TileElement* tileElement = it.current.element;
            auto trackType = tileElement->asTrack()->getTrackType();
            switch (trackType)
            {
                case TrackElemType::up25:
                case TrackElemType::up60:
                case TrackElemType::flatToUp25:
                case TrackElemType::up25ToFlat:
                case TrackElemType::up25ToUp60:
                case TrackElemType::up60ToUp25:
                case TrackElemType::flatToUp60LongBase:
                case TrackElemType::flat:
                    if (isApplying)
                    {
                        GetTrackElementOriginAndApplyChanges(
                            { { it.current, tileElement->getBaseZ() }, tileElement->getDirection() }, trackType, 0,
                            &tileElement, { TrackElementSetFlag::cableLiftOn });
                    }
                    break;
                case TrackElemType::endStation:
                case TrackElemType::blockBrakes:
                    return { true };
                default:
                    return { false, STR_CABLE_LIFT_HILL_MUST_START_IMMEDIATELY_AFTER_STATION_OR_BLOCK_BRAKE };
            }
        }
        return { false, STR_CABLE_LIFT_HILL_MUST_START_IMMEDIATELY_AFTER_STATION_OR_BLOCK_BRAKE };
    }

    /**
     *
     *  rct2: 0x006DF4D4
     */
    static ResultWithMessage RideCreateCableLift(RideId rideIndex, bool isApplying)
    {
        auto ride = GetRide(rideIndex);
        if (ride == nullptr)
            return { false };

        if (ride->mode != RideMode::continuousCircuitBlockSectioned && ride->mode != RideMode::continuousCircuit)
        {
            return { false, STR_CABLE_LIFT_UNABLE_TO_WORK_IN_THIS_OPERATING_MODE };
        }

        if (ride->numCircuits > 1)
        {
            return { false, STR_MULTICIRCUIT_NOT_POSSIBLE_WITH_CABLE_LIFT_HILL };
        }

        if (count_free_misc_sprite_slots() <= 5)
        {
            return { false, STR_UNABLE_TO_CREATE_ENOUGH_VEHICLES };
        }

        auto cableLiftInitialiseResult = RideInitialiseCableLiftTrack(*ride, isApplying);
        if (!cableLiftInitialiseResult.Successful)
        {
            return { false, cableLiftInitialiseResult.Message };
        }

        if (!isApplying)
        {
            return { true };
        }

        auto cableLiftLoc = ride->cableLiftLoc;
        auto tileElement = MapGetTrackElementAt(cableLiftLoc);
        int32_t direction = tileElement->getDirection();

        Vehicle* head = nullptr;
        Vehicle* tail = nullptr;
        uint32_t ebx = 0;
        for (int32_t i = 0; i < 5; i++)
        {
            uint32_t edx = Numerics::ror32(0x15478, 10);
            uint16_t var_44 = edx & 0xFFFF;
            edx = Numerics::rol32(edx, 10) >> 1;
            ebx -= edx;
            int32_t remaining_distance = ebx;
            ebx -= edx;

            Vehicle* current = CableLiftSegmentCreate(
                *ride, cableLiftLoc.x, cableLiftLoc.y, cableLiftLoc.z / 8, direction, var_44, remaining_distance, i == 0);
            current->next_vehicle_on_train = EntityId::GetNull();
            if (i == 0)
            {
                head = current;
            }
            else
            {
                tail->next_vehicle_on_train = current->id;
                tail->next_vehicle_on_ride = current->id;
                current->prev_vehicle_on_ride = tail->id;
            }
            tail = current;
        }
        head->prev_vehicle_on_ride = tail->id;
        tail->next_vehicle_on_ride = head->id;

        ride->flags.set(RideFlag::cableLift);
        head->CableLiftUpdateTrackMotion();
        return { true };
    }

    /**
     * Opens the construction window prompting to construct a missing entrance or exit.
     * This will also move the screen to the first station missing the entrance or exit.
     *  rct2: 0x006B51C0
     */
    void Ride::constructMissingEntranceOrExit() const
    {
        auto* w = WindowGetMain();
        if (w == nullptr)
            return;

        int8_t entranceOrExit = -1;
        const RideStation* incompleteStation = nullptr;
        for (const auto& station : stations)
        {
            if (station.Start.IsNull())
                continue;

            if (station.Entrance.IsNull())
            {
                entranceOrExit = WC_RIDE_CONSTRUCTION__WIDX_ENTRANCE;
                incompleteStation = &station;
                break;
            }

            if (station.Exit.IsNull())
            {
                entranceOrExit = WC_RIDE_CONSTRUCTION__WIDX_EXIT;
                incompleteStation = &station;
                break;
            }
        }

        if (incompleteStation == nullptr)
        { // No station with a missing entrance or exit was found
            return;
        }

        const auto& rtd = getRideTypeDescriptor();
        if (rtd.specialType != RtdSpecialType::maze)
        {
            auto location = incompleteStation->GetStart();
            WindowScrollToLocation(*w, location);

            CoordsXYE trackElement;
            RideTryGetOriginElement(*this, &trackElement);
            findTrackGap(*this, trackElement, &trackElement);
            int32_t ok = RideModify(trackElement);
            if (ok == 0)
            {
                return;
            }

            auto* windowMgr = Ui::GetWindowManager();
            w = windowMgr->FindByClass(WindowClass::rideConstruction);
            if (w != nullptr)
                w->onMouseUp(entranceOrExit);
        }
    }

    /**
     *
     *  rct2: 0x006B528A
     */
    static void RideScrollToTrackError(const CoordsXYE& trackElement)
    {
        if (trackElement.element == nullptr)
            return;

        auto* w = WindowGetMain();
        if (w != nullptr)
        {
            WindowScrollToLocation(*w, { trackElement, trackElement.element->getBaseZ() });
            RideModify(trackElement);
        }
    }

    /**
     *
     *  rct2: 0x006B4F6B
     */
    TrackElement* Ride::getOriginElement(StationIndex stationIndex) const
    {
        auto stationLoc = getStation(stationIndex).Start;
        TileElement* tileElement = MapGetFirstElementAt(stationLoc);
        if (tileElement == nullptr)
            return nullptr;
        do
        {
            if (tileElement->getType() != TileElementType::track)
                continue;

            auto* trackElement = tileElement->asTrack();
            const auto& ted = GetTrackElementDescriptor(trackElement->getTrackType());
            if (!ted.sequenceData.sequences[0].flags.has(SequenceFlag::trackOrigin))
                continue;

            if (trackElement->getRideIndex() == id)
                return trackElement;
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    ResultWithMessage Ride::test(bool isApplying)
    {
        if (type == kRideTypeNull)
        {
            LOG_WARNING("Invalid ride type for ride %u", id.ToUnderlying());
            return { false };
        }

        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->CloseByNumber(WindowClass::rideConstruction, id.ToUnderlying());

        StationIndex stationIndex = {};
        auto message = changeStatusDoStationChecks(stationIndex);
        if (!message.Successful)
        {
            return message;
        }

        auto entranceExitCheck = RideCheckForEntranceExit(id);
        if (!entranceExitCheck.Successful)
        {
            constructMissingEntranceOrExit();
            return { false, entranceExitCheck.Message };
        }

        CoordsXYE trackElement = {};
        message = changeStatusGetStartElement(stationIndex, trackElement);
        if (!message.Successful)
        {
            return message;
        }

        message = changeStatusCheckCompleteCircuit(trackElement);
        if (!message.Successful)
        {
            return message;
        }

        message = changeStatusCheckTrackValidity(trackElement, false);
        if (!message.Successful)
        {
            return message;
        }

        return changeStatusCreateVehicles(isApplying, trackElement, false);
    }

    ResultWithMessage Ride::simulate(bool isApplying)
    {
        CoordsXYE trackElement = {};
        if (type == kRideTypeNull)
        {
            LOG_WARNING("Invalid ride type for ride %u", id.ToUnderlying());
            return { false };
        }

        StationIndex stationIndex = {};
        auto message = changeStatusDoStationChecks(stationIndex);
        if (!message.Successful)
        {
            return message;
        }

        message = changeStatusGetStartElement(stationIndex, trackElement);
        if (!message.Successful)
        {
            return message;
        }

        message = changeStatusCheckTrackValidity(trackElement, true);
        if (!message.Successful)
        {
            return message;
        }

        return changeStatusCreateVehicles(isApplying, trackElement, true);
    }

    /**
     *
     *  rct2: 0x006B4EEA
     */
    ResultWithMessage Ride::open(bool isApplying)
    {
        // Check to see if construction tool is in use. If it is close the construction window
        // to set the track to its final state and clean up ghosts.
        // We can't just call close as it would cause a stack overflow during shop creation
        // with auto open on.
        if (isToolActive(WindowClass::rideConstruction, static_cast<WindowNumber>(id.ToUnderlying())))
        {
            auto* windowMgr = Ui::GetWindowManager();
            windowMgr->CloseByNumber(WindowClass::rideConstruction, id.ToUnderlying());
        }

        StationIndex stationIndex = {};
        auto message = changeStatusDoStationChecks(stationIndex);
        if (!message.Successful)
        {
            return message;
        }

        auto entranceExitCheck = RideCheckForEntranceExit(id);
        if (!entranceExitCheck.Successful)
        {
            constructMissingEntranceOrExit();
            return { false, entranceExitCheck.Message };
        }

        if (isApplying)
        {
            chainQueues();
            flags.set(RideFlag::everBeenOpened);
        }

        CoordsXYE trackElement = {};
        message = changeStatusGetStartElement(stationIndex, trackElement);
        if (!message.Successful)
        {
            return message;
        }

        message = changeStatusCheckCompleteCircuit(trackElement);
        if (!message.Successful)
        {
            return message;
        }

        message = changeStatusCheckTrackValidity(trackElement, false);
        if (!message.Successful)
        {
            return message;
        }

        return changeStatusCreateVehicles(isApplying, trackElement, false);
    }

    /**
     * Given a track element of the ride, find the start of the track.
     * It has to do this as a backwards loop in case this is an incomplete track.
     */
    void RideGetStartOfTrack(CoordsXYE* output)
    {
        TrackBeginEnd trackBeginEnd;
        CoordsXYE trackElement = *output;
        if (trackBlockGetPrevious(trackElement, &trackBeginEnd))
        {
            TileElement* initial_map = trackElement.element;
            TrackBeginEnd slowIt = trackBeginEnd;
            bool moveSlowIt = true;
            do
            {
                // Because we are working backwards, begin_element is the section at the end of a piece of track, whereas
                // begin_x and begin_y are the coordinates at the start of a piece of track, so we need to pass end_x and
                // end_y
                CoordsXYE lastGood = {
                    /* .x = */ trackBeginEnd.end_x,
                    /* .y = */ trackBeginEnd.end_y,
                    /* .element = */ trackBeginEnd.begin_element,
                };

                if (!trackBlockGetPrevious(
                        { trackBeginEnd.end_x, trackBeginEnd.end_y, trackBeginEnd.begin_element }, &trackBeginEnd))
                {
                    trackElement = lastGood;
                    break;
                }

                moveSlowIt = !moveSlowIt;
                if (moveSlowIt)
                {
                    if (!trackBlockGetPrevious({ slowIt.end_x, slowIt.end_y, slowIt.begin_element }, &slowIt)
                        || slowIt.begin_element == trackBeginEnd.begin_element)
                    {
                        break;
                    }
                }
            } while (initial_map != trackBeginEnd.begin_element);
        }
        *output = trackElement;
    }

    /**
     *
     *  rct2: 0x00696707
     */
    void Ride::stopGuestsQueuing()
    {
        for (auto peep : EntityList<Guest>())
        {
            if (peep->state != PeepState::queuing)
                continue;
            if (peep->currentRide != id)
                continue;

            peep->removeFromQueue();
            peep->setState(PeepState::falling);
        }
    }

    RideMode Ride::getDefaultMode() const
    {
        return getRideTypeDescriptor().DefaultMode;
    }

    static bool RideTypeWithTrackColoursExists(ride_type_t rideType, const TrackColour& colours)
    {
        auto& gameState = getGameState();
        for (auto& ride : RideManager(gameState))
        {
            if (ride.type != rideType)
                continue;
            if (ride.trackColours[0].main != colours.main)
                continue;
            if (ride.trackColours[0].additional != colours.additional)
                continue;
            if (ride.trackColours[0].supports != colours.supports)
                continue;

            return true;
        }
        return false;
    }

    bool Ride::nameExists(std::string_view name, RideId excludeRideId)
    {
        auto& gameState = getGameState();
        for (auto& ride : RideManager(gameState))
        {
            if (ride.id != excludeRideId)
            {
                Formatter ft;
                ride.formatNameTo(ft);

                char buffer[256]{};
                FormatStringLegacy(buffer, 256, STR_STRINGID, ft.Data());
                if (name == buffer && RideHasAnyTrackElements(ride))
                {
                    return true;
                }
            }
        }
        return false;
    }

    int32_t RideGetRandomColourPresetIndex(ride_type_t rideType)
    {
        if (rideType >= std::size(kRideTypeDescriptors))
        {
            return 0;
        }

        // Find all the presets that haven't yet been used in the park for this ride type
        const auto& colourPresets = GetRideTypeDescriptor(rideType).ColourPresets;
        std::vector<uint8_t> unused;
        unused.reserve(colourPresets.count);
        for (uint8_t i = 0; i < colourPresets.count; i++)
        {
            const auto& colours = colourPresets.list[i];
            if (!RideTypeWithTrackColoursExists(rideType, colours))
            {
                unused.push_back(static_cast<uint8_t>(i));
            }
        }

        // If all presets have been used, just go with a random preset
        if (unused.empty())
            return UtilRand() % colourPresets.count;

        // Choose a random preset from the list of unused presets
        auto unusedIndex = UtilRand() % unused.size();
        return unused[unusedIndex];
    }

    /**
     *
     *  Based on rct2: 0x006B4776
     */
    void Ride::setColourPreset(uint8_t trackColourPreset, uint8_t vehicleColourPreset)
    {
        const TrackColourPresetList* colourPresets = &getRideTypeDescriptor().ColourPresets;
        TrackColour colours = { Drawing::Colour::black, Drawing::Colour::black, Drawing::Colour::black };
        // Stalls save their default colour in the vehicle settings (since they share a common ride type)
        if (!isRide())
        {
            const auto* rideEntry = GetRideEntryByIndex(subtype);
            if (rideEntry != nullptr && rideEntry->vehicle_preset_list->count > 0)
            {
                if (vehicleColourPreset < rideEntry->vehicle_preset_list->count)
                {
                    auto list = rideEntry->vehicle_preset_list->list[vehicleColourPreset];
                    colours = { list.Body, list.Trim, list.Tertiary };
                }
            }
        }
        else if (trackColourPreset < colourPresets->count)
        {
            colours = colourPresets->list[trackColourPreset];
        }
        for (size_t i = 0; i < std::size(trackColours); i++)
        {
            trackColours[i].main = colours.main;
            trackColours[i].additional = colours.additional;
            trackColours[i].supports = colours.supports;
        }
        vehicleColourSettings = VehicleColourSettings::same;
    }

    money64 RideGetCommonPrice(const Ride& forRide)
    {
        auto& gameState = getGameState();
        for (const auto& ride : RideManager(gameState))
        {
            if (ride.type == forRide.type && ride.id != forRide.id)
            {
                return ride.price[0];
            }
        }

        return kMoney64Undefined;
    }

    void Ride::setNameToDefault()
    {
        char rideNameBuffer[256]{};

        // Increment default name number until we find a unique name
        customName = {};
        defaultNameNumber = 0;
        do
        {
            defaultNameNumber++;
            Formatter ft;
            formatNameTo(ft);
            FormatStringLegacy(rideNameBuffer, 256, STR_STRINGID, ft.Data());
        } while (nameExists(rideNameBuffer, id));
    }

    /**
     * This will return the name of the ride, as seen in the New Ride window.
     */
    RideNaming GetRideNaming(const ride_type_t rideType, const RideObjectEntry* rideEntry)
    {
        const auto& rtd = GetRideTypeDescriptor(rideType);
        if (rtd.flags.has(RtdFlag::listVehiclesSeparately) && rideEntry != nullptr)
        {
            return rideEntry->naming;
        }

        return rtd.Naming;
    }

    /*
     * The next eight functions are helpers to access ride data at the offset 10E &
     * 110. Known as the turn counts. There are 3 different types (default, banked, sloped)
     * and there are 4 counts as follows:
     *
     * 1 element turns: low 5 bits
     * 2 element turns: bits 6-8
     * 3 element turns: bits 9-11
     * 4 element or more turns: bits 12-15
     *
     * 4 plus elements only possible on sloped type. Falls back to 3 element
     * if by some miracle you manage 4 element none sloped.
     */

    void IncrementTurnCount1Element(Ride& ride, uint8_t type)
    {
        uint16_t* turn_count;
        switch (type)
        {
            case 0:
                turn_count = &ride.turnCountDefault;
                break;
            case 1:
                turn_count = &ride.turnCountBanked;
                break;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return;
        }
        uint16_t value = (*turn_count & kTurnMask1Element) + 1;
        *turn_count &= ~kTurnMask1Element;

        if (value > kTurnMask1Element)
            value = kTurnMask1Element;
        *turn_count |= value;
    }

    void IncrementTurnCount2Elements(Ride& ride, uint8_t type)
    {
        uint16_t* turn_count;
        switch (type)
        {
            case 0:
                turn_count = &ride.turnCountDefault;
                break;
            case 1:
                turn_count = &ride.turnCountBanked;
                break;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return;
        }
        uint16_t value = (*turn_count & kTurnMask2Elements) + 0x20;
        *turn_count &= ~kTurnMask2Elements;

        if (value > kTurnMask2Elements)
            value = kTurnMask2Elements;
        *turn_count |= value;
    }

    void IncrementTurnCount3Elements(Ride& ride, uint8_t type)
    {
        uint16_t* turn_count;
        switch (type)
        {
            case 0:
                turn_count = &ride.turnCountDefault;
                break;
            case 1:
                turn_count = &ride.turnCountBanked;
                break;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return;
        }
        uint16_t value = (*turn_count & kTurnMask3Elements) + 0x100;
        *turn_count &= ~kTurnMask3Elements;

        if (value > kTurnMask3Elements)
            value = kTurnMask3Elements;
        *turn_count |= value;
    }

    void IncrementTurnCount4PlusElements(Ride& ride, uint8_t type)
    {
        uint16_t* turn_count;
        switch (type)
        {
            case 0:
            case 1:
                // Just in case fallback to 3 element turn
                IncrementTurnCount3Elements(ride, type);
                return;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return;
        }
        uint16_t value = (*turn_count & kTurnMask4PlusElements) + 0x800;
        *turn_count &= ~kTurnMask4PlusElements;

        if (value > kTurnMask4PlusElements)
            value = kTurnMask4PlusElements;
        *turn_count |= value;
    }

    int32_t GetTurnCount1Element(const Ride& ride, uint8_t type)
    {
        const uint16_t* turn_count;
        switch (type)
        {
            case 0:
                turn_count = &ride.turnCountDefault;
                break;
            case 1:
                turn_count = &ride.turnCountBanked;
                break;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return 0;
        }

        return (*turn_count) & kTurnMask1Element;
    }

    int32_t GetTurnCount2Elements(const Ride& ride, uint8_t type)
    {
        const uint16_t* turn_count;
        switch (type)
        {
            case 0:
                turn_count = &ride.turnCountDefault;
                break;
            case 1:
                turn_count = &ride.turnCountBanked;
                break;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return 0;
        }

        return ((*turn_count) & kTurnMask2Elements) >> 5;
    }

    int32_t GetTurnCount3Elements(const Ride& ride, uint8_t type)
    {
        const uint16_t* turn_count;
        switch (type)
        {
            case 0:
                turn_count = &ride.turnCountDefault;
                break;
            case 1:
                turn_count = &ride.turnCountBanked;
                break;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return 0;
        }

        return ((*turn_count) & kTurnMask3Elements) >> 8;
    }

    int32_t GetTurnCount4PlusElements(const Ride& ride, uint8_t type)
    {
        const uint16_t* turn_count;
        switch (type)
        {
            case 0:
            case 1:
                return 0;
            case 2:
                turn_count = &ride.turnCountSloped;
                break;
            default:
                return 0;
        }

        return ((*turn_count) & kTurnMask4PlusElements) >> 11;
    }

    bool Ride::hasSpinningTunnel() const
    {
        return specialTrackElements.has(SpecialElement::spinningTunnel);
    }

    bool Ride::hasWaterSplash() const
    {
        return specialTrackElements.has(SpecialElement::splash);
    }

    bool Ride::hasRapids() const
    {
        return specialTrackElements.has(SpecialElement::rapids);
    }

    bool Ride::hasLogReverser() const
    {
        return specialTrackElements.has(SpecialElement::reverser);
    }

    bool Ride::hasWaterfall() const
    {
        return specialTrackElements.has(SpecialElement::waterfall);
    }

    bool Ride::hasWhirlpool() const
    {
        return specialTrackElements.has(SpecialElement::whirlpool);
    }

    bool Ride::isPoweredLaunched() const
    {
        return mode == RideMode::poweredLaunchPassthrough || mode == RideMode::poweredLaunch
            || mode == RideMode::poweredLaunchBlockSectioned;
    }

    bool Ride::isBlockSectioned() const
    {
        return mode == RideMode::continuousCircuitBlockSectioned || mode == RideMode::poweredLaunchBlockSectioned;
    }

    bool RideHasAnyTrackElements(const Ride& ride)
    {
        TileElementIterator it;

        TileElementIteratorBegin(&it);
        while (TileElementIteratorNext(&it))
        {
            if (it.element->getType() != TileElementType::track)
                continue;
            if (it.element->asTrack()->getRideIndex() != ride.id)
                continue;
            if (it.element->isGhost())
                continue;

            return true;
        }

        return false;
    }

    /**
     *
     *  rct2: 0x006B59C6
     */
    void InvalidateTestResults(Ride& ride)
    {
        ride.measurement = {};
        ride.ratings.setNull();
        ride.flags.unset(RideFlag::tested, RideFlag::testInProgress);
        if (ride.flags.has(RideFlag::onTrack))
        {
            for (int32_t i = 0; i < ride.numTrains; i++)
            {
                Vehicle* vehicle = getGameState().entities.getEntity<Vehicle>(ride.vehicles[i]);
                if (vehicle != nullptr)
                {
                    vehicle->flags.unset(VehicleFlag::testing);
                }
            }
        }

        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->InvalidateByNumber(WindowClass::ride, ride.id.ToUnderlying());
    }

    /**
     *
     *  rct2: 0x006B7481
     *
     * @param rideIndex (dl)
     * @param reliabilityIncreaseFactor (ax)
     */
    void RideFixBreakdown(Ride& ride, int32_t reliabilityIncreaseFactor)
    {
        ride.flags.unset(RideFlag::breakdownPending, RideFlag::brokenDown, RideFlag::dueInspection);
        ride.windowInvalidateFlags.set(RideInvalidateFlag::main, RideInvalidateFlag::list, RideInvalidateFlag::maintenance);

        if (ride.flags.has(RideFlag::onTrack))
        {
            for (int32_t i = 0; i < ride.numTrains; i++)
            {
                for (Vehicle* vehicle = getGameState().entities.getEntity<Vehicle>(ride.vehicles[i]); vehicle != nullptr;
                     vehicle = getGameState().entities.getEntity<Vehicle>(vehicle->next_vehicle_on_train))
                {
                    vehicle->flags.unset(
                        VehicleFlag::stoppedBySafetyCutout, VehicleFlag::carIsBroken, VehicleFlag::trainIsBroken);
                }
            }
        }

        uint8_t unreliability = 100 - ride.reliabilityPercentage;
        ride.reliability += reliabilityIncreaseFactor * (unreliability / 2);
    }

    /**
     *
     *  rct2: 0x006DE102
     */
    void RideUpdateVehicleColours(const Ride& ride)
    {
        const auto& rtd = ride.getRideTypeDescriptor();
        if (rtd.specialType == RtdSpecialType::spaceRings || rtd.flags.has(RtdFlag::vehicleIsIntegral))
        {
            GfxInvalidateScreen();
        }

        auto& entities = getGameState().entities;
        for (int32_t i = 0; i <= Limits::kMaxTrainsPerRide; i++)
        {
            int32_t carIndex = 0;
            for (Vehicle* vehicle = entities.getEntity<Vehicle>(ride.vehicles[i]); vehicle != nullptr;
                 vehicle = entities.getEntity<Vehicle>(vehicle->next_vehicle_on_train))
            {
                VehicleColour colours = {};
                switch (ride.vehicleColourSettings)
                {
                    case VehicleColourSettings::same:
                        colours = ride.vehicleColours[0];
                        break;
                    case VehicleColourSettings::perTrain:
                        colours = ride.vehicleColours[i];
                        break;
                    case VehicleColourSettings::perCar:
                    {
                        const bool isReversed = vehicle->flags.has(VehicleFlag::carIsReversed);
                        const auto colourIndex = isReversed ? (ride.numCarsPerTrain - 1) - carIndex : carIndex;
                        colours = ride.vehicleColours[std::min(colourIndex, Limits::kMaxCarsPerTrain - 1)];
                        break;
                    }
                }

                vehicle->colours = colours;
                vehicle->invalidate();
                carIndex++;
            }
        }
    }

    uint8_t RideEntryGetVehicleAtPosition(int32_t rideEntryIndex, int32_t numCarsPerTrain, int32_t position)
    {
        const auto* rideEntry = GetRideEntryByIndex(rideEntryIndex);
        if (position == 0 && rideEntry->FrontCar != 255)
        {
            return rideEntry->FrontCar;
        }
        if (position == 1 && rideEntry->SecondCar != 255)
        {
            return rideEntry->SecondCar;
        }
        if (position == 2 && rideEntry->ThirdCar != 255)
        {
            return rideEntry->ThirdCar;
        }
        if (position == numCarsPerTrain - 1 && rideEntry->RearCar != 255)
        {
            return rideEntry->RearCar;
        }

        return rideEntry->DefaultCar;
    }

    struct NecessarySpriteGroup
    {
        SpriteGroupType VehicleSpriteGroup;
        SpritePrecision MinPrecision;
    };

    // Finds track pieces that a given ride entry has sprites for
    BitSet<EnumValue(TrackGroup::count)> RideEntryGetSupportedTrackPieces(const RideObjectEntry& rideEntry)
    {
        // TODO: Use a std::span when C++20 available as 6 is due to jagged array
        static const std::array<NecessarySpriteGroup, 9> trackPieceRequiredSprites[] = {
            { SpriteGroupType::slopeFlat, SpritePrecision::none },     // TrackGroup::flat
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 }, // TrackGroup::straight
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 }, // TrackGroup::stationEnd
            { SpriteGroupType::slopes25, SpritePrecision::sprites4 },  // TrackGroup::liftHill
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60,
              SpritePrecision::sprites4 },                             // TrackGroup::liftHillSteep
            { SpriteGroupType::slopes25, SpritePrecision::sprites16 }, // TrackGroup::liftHillCurve
            { SpriteGroupType::flatBanked22, SpritePrecision::sprites4, SpriteGroupType::flatBanked45,
              SpritePrecision::sprites16 }, // TrackGroup::flatRollBanking
            { SpriteGroupType::slopes60, SpritePrecision::sprites4, SpriteGroupType::slopes75, SpritePrecision::sprites4,
              SpriteGroupType::slopes90, SpritePrecision::sprites4, SpriteGroupType::slopesLoop, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::verticalLoop
            { SpriteGroupType::slopes25, SpritePrecision::sprites4 },      // TrackGroup::slope
            { SpriteGroupType::slopes60, SpritePrecision::sprites4 },      // TrackGroup::slopeSteepDown
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60,
              SpritePrecision::sprites4 },                              // TrackGroup::flatToSteepSlope
            { SpriteGroupType::slopes25, SpritePrecision::sprites16 },  // TrackGroup::slopeCurve
            { SpriteGroupType::slopes60, SpritePrecision::sprites16 },  // TrackGroup::slopeCurveSteep
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 }, // TrackGroup::sBend
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 }, // TrackGroup::curveVerySmall
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 }, // TrackGroup::curveSmall
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 }, // TrackGroup::curve
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 }, // TrackGroup::curveLarge
            { SpriteGroupType::flatBanked22, SpritePrecision::sprites4, SpriteGroupType::flatBanked45,
              SpritePrecision::sprites4, SpriteGroupType::flatBanked67, SpritePrecision::sprites4,
              SpriteGroupType::flatBanked90, SpritePrecision::sprites4, SpriteGroupType::inlineTwists,
              SpritePrecision::sprites4, SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::twist
            { SpriteGroupType::slopes60, SpritePrecision::sprites4, SpriteGroupType::slopes75, SpritePrecision::sprites4,
              SpriteGroupType::slopes90, SpritePrecision::sprites4, SpriteGroupType::slopesLoop, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::halfLoop
            { SpriteGroupType::corkscrews, SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 },                                 // TrackGroup::corkscrew
            { SpriteGroupType::slopeFlat, SpritePrecision::none },         // TrackGroup::tower
            { SpriteGroupType::flatBanked45, SpritePrecision::sprites16 }, // TrackGroup::helixUpBankedHalf
            { SpriteGroupType::flatBanked45, SpritePrecision::sprites16 }, // TrackGroup::helixDownBankedHalf
            { SpriteGroupType::flatBanked45, SpritePrecision::sprites16 }, // TrackGroup::helixUpBankedQuarter
            { SpriteGroupType::flatBanked45, SpritePrecision::sprites16 }, // TrackGroup::helixDownBankedQuarter
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 },    // TrackGroup::helixUpUnbankedQuarter
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 },    // TrackGroup::helixDownUnbankedQuarter
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 },     // TrackGroup::brakes
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 },     // TrackGroup::onridePhoto
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4, SpriteGroupType::slopes12,
              SpritePrecision::sprites4 }, // TrackGroup::waterSplash
            { SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 }, // TrackGroup::slopeVertical
            { SpriteGroupType::flatBanked22, SpritePrecision::sprites4, SpriteGroupType::flatBanked45,
              SpritePrecision::sprites4, SpriteGroupType::inlineTwists, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::barrelRoll
            { SpriteGroupType::slopes25, SpritePrecision::sprites4 },      // TrackGroup::poweredLift
            { SpriteGroupType::slopes60, SpritePrecision::sprites4, SpriteGroupType::slopes75, SpritePrecision::sprites4,
              SpriteGroupType::slopes90, SpritePrecision::sprites4, SpriteGroupType::slopesLoop, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 },     // TrackGroup::halfLoopLarge
            { SpriteGroupType::slopes12Banked22, SpritePrecision::sprites16 }, // TrackGroup::slopeCurveBanked
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 },        // TrackGroup::logFlumeReverser
            { SpriteGroupType::flatBanked22, SpritePrecision::sprites4, SpriteGroupType::flatBanked45,
              SpritePrecision::sprites4, SpriteGroupType::inlineTwists, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::heartlineRoll
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites16 },    // TrackGroup::reverser
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4, SpriteGroupType::slopes25, SpritePrecision::sprites4,
              SpriteGroupType::slopes60, SpritePrecision::sprites4, SpriteGroupType::slopes75, SpritePrecision::sprites4,
              SpriteGroupType::slopes90, SpritePrecision::sprites4 }, // TrackGroup::reverseFreefall
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4, SpriteGroupType::slopes25, SpritePrecision::sprites4,
              SpriteGroupType::slopes60, SpritePrecision::sprites4, SpriteGroupType::slopes75, SpritePrecision::sprites4,
              SpriteGroupType::slopes90, SpritePrecision::sprites4 },         // TrackGroup::slopeToFlat
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 },        // TrackGroup::blockBrakes
            { SpriteGroupType::slopes25Banked22, SpritePrecision::sprites4 }, // TrackGroup::slopeRollBanking
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60,
              SpritePrecision::sprites4 },                             // TrackGroup::slopeSteepLong
            { SpriteGroupType::slopes90, SpritePrecision::sprites16 }, // TrackGroup::curveVertical
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60,
              SpritePrecision::sprites4 },                                     // TrackGroup::liftHillCable
            { SpriteGroupType::curvedLiftHillUp, SpritePrecision::sprites16 }, // TrackGroup::liftHillCurved
            { SpriteGroupType::slopes90, SpritePrecision::sprites4, SpriteGroupType::slopesLoop, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::quarterLoop
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 },     // TrackGroup::spinningTunnel
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 },     // TrackGroup::booster
            { SpriteGroupType::flatBanked22, SpritePrecision::sprites4, SpriteGroupType::flatBanked45,
              SpritePrecision::sprites4, SpriteGroupType::flatBanked67, SpritePrecision::sprites4,
              SpriteGroupType::flatBanked90, SpritePrecision::sprites4, SpriteGroupType::inlineTwists,
              SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 }, // TrackGroup::inlineTwistUninverted
            { SpriteGroupType::flatBanked22, SpritePrecision::sprites4, SpriteGroupType::flatBanked45,
              SpritePrecision::sprites4, SpriteGroupType::flatBanked67, SpritePrecision::sprites4,
              SpriteGroupType::flatBanked90, SpritePrecision::sprites4, SpriteGroupType::inlineTwists,
              SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 }, // TrackGroup::inlineTwistInverted
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 }, // TrackGroup::quarterLoopUninvertedUp
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 }, // TrackGroup::quarterLoopUninvertedDown
            { SpriteGroupType::slopes90, SpritePrecision::sprites4, SpriteGroupType::slopesLoop, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::quarterLoopInvertedUp
            { SpriteGroupType::slopes90, SpritePrecision::sprites4, SpriteGroupType::slopesLoop, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::quarterLoopInvertedDown
            { SpriteGroupType::slopes12, SpritePrecision::sprites4 },      // TrackGroup::rapids
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 }, // TrackGroup::flyingHalfLoopUninvertedUp
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90, SpritePrecision::sprites4,
              SpriteGroupType::slopesLoop, SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 },                             // TrackGroup::flyingHalfLoopInvertedDown
            {},                                                        // TrackGroup::flatRideBase
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 }, // TrackGroup::waterfall
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 }, // TrackGroup::whirlpool
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60,
              SpritePrecision::sprites4 }, // TrackGroup::brakeForDrop
            { SpriteGroupType::corkscrews, SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 }, // TrackGroup::corkscrewUninverted
            { SpriteGroupType::corkscrews, SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 }, // TrackGroup::corkscrewInverted
            { SpriteGroupType::slopes12, SpritePrecision::sprites4, SpriteGroupType::slopes25,
              SpritePrecision::sprites4 },                             // TrackGroup::heartlineTransfer
            {},                                                        // TrackGroup::miniGolfHole
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites4 }, // TrackGroup::rotationControlToggle
            { SpriteGroupType::slopes60, SpritePrecision::sprites4 },  // TrackGroup::slopeSteepUp
            { SpriteGroupType::corkscrews, SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 }, // TrackGroup::corkscrewLarge
            { SpriteGroupType::slopes60, SpritePrecision::sprites4, SpriteGroupType::slopes75, SpritePrecision::sprites4,
              SpriteGroupType::slopes90, SpritePrecision::sprites4, SpriteGroupType::slopesLoop, SpritePrecision::sprites4,
              SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::halfLoopMedium
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes12Banked22,
              SpritePrecision::sprites4, SpriteGroupType::slopes25Banked22, SpritePrecision::sprites4,
              SpriteGroupType::slopes25Banked45, SpritePrecision::sprites4, SpriteGroupType::inlineTwists,
              SpritePrecision::sprites4, SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::zeroGRoll
            { SpriteGroupType::slopes42Banked22, SpritePrecision::sprites4, SpriteGroupType::slopes42Banked45,
              SpritePrecision::sprites4, SpriteGroupType::slopes42Banked67, SpritePrecision::sprites4,
              SpriteGroupType::slopes42Banked90, SpritePrecision::sprites4, SpriteGroupType::slopes60Banked22,
              SpritePrecision::sprites4 }, // TrackGroup::zeroGRollLarge
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 }, // TrackGroup::flyingLargeHalfLoopUninvertedUp
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90, SpritePrecision::sprites4,
              SpriteGroupType::slopesLoop, SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 }, // TrackGroup::flyingLargeHalfLoopInvertedDown
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90, SpritePrecision::sprites4,
              SpriteGroupType::slopesLoop, SpritePrecision::sprites4, SpriteGroupType::slopeInverted,
              SpritePrecision::sprites4 }, // TrackGroup::flyingLargeHalfLoopUninvertedDown
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 }, // TrackGroup::flyingLargeHalfLoopInvertedUp
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 }, // TrackGroup::flyingHalfLoopInvertedUp
            { SpriteGroupType::slopes25, SpritePrecision::sprites4, SpriteGroupType::slopes60, SpritePrecision::sprites4,
              SpriteGroupType::slopes75, SpritePrecision::sprites4, SpriteGroupType::slopes90,
              SpritePrecision::sprites4 },                                     // TrackGroup::flyingHalfLoopUninvertedDown
            { SpriteGroupType::slopes25, SpritePrecision::sprites16 },         // TrackGroup::slopeCurveLarge
            { SpriteGroupType::slopes25Banked45, SpritePrecision::sprites16 }, // TrackGroup::slopeCurveLargeBanked
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites8 },         // TrackGroup::diagBrakes
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites8 },         // TrackGroup::diagBlockBrakes
            { SpriteGroupType::slopes25, SpritePrecision::sprites8 },          // TrackGroup::inclinedBrakes
            { SpriteGroupType::slopeFlat, SpritePrecision::sprites8 },         // TrackGroup::diagBooster
            { SpriteGroupType::slopes8, SpritePrecision::sprites4, SpriteGroupType::slopes16, SpritePrecision::sprites4,
              SpriteGroupType::slopes25, SpritePrecision::sprites8, SpriteGroupType::slopes42, SpritePrecision::sprites8,
              SpriteGroupType::slopes50, SpritePrecision::sprites4 }, // TrackGroup::slopeSteepLong
            { SpriteGroupType::slopes50, SpritePrecision::sprites4, SpriteGroupType::slopes60Banked22,
              SpritePrecision::sprites8, SpriteGroupType::slopes50Banked45, SpritePrecision::sprites8,
              SpriteGroupType::slopes50Banked67, SpritePrecision::sprites8, SpriteGroupType::slopes50Banked90,
              SpritePrecision::sprites8, SpriteGroupType::corkscrews, SpritePrecision::sprites4,
              SpriteGroupType::slopes25InlineTwists, SpritePrecision::sprites4, SpriteGroupType::slopesLoop,
              SpritePrecision::sprites4, SpriteGroupType::slopeInverted, SpritePrecision::sprites4 }, // TrackGroup::diveLoop
            { SpriteGroupType::slopes8, SpritePrecision::sprites4, SpriteGroupType::slopes16,
              SpritePrecision::sprites4 }, // TrackGroup::diagSlope
            { SpriteGroupType::slopes25, SpritePrecision::sprites8, SpriteGroupType::slopes42, SpritePrecision::sprites8,
              SpriteGroupType::slopes50, SpritePrecision::sprites4 }, // TrackGroup::diagSlopeSteepUp
            { SpriteGroupType::slopes25, SpritePrecision::sprites8, SpriteGroupType::slopes42, SpritePrecision::sprites8,
              SpriteGroupType::slopes50, SpritePrecision::sprites4 }, // TrackGroup::diagSlopeSteepDown
        };

        static_assert(std::size(trackPieceRequiredSprites) == EnumValue(TrackGroup::count));

        // Only check default vehicle; it's assumed the others will have correct sprites if this one does (I've yet to find an
        // exception, at least)
        auto supportedPieces = BitSet<EnumValue(TrackGroup::count)>();
        supportedPieces.flip();
        auto defaultVehicle = rideEntry.GetDefaultCar();
        if (defaultVehicle != nullptr)
        {
            for (size_t i = 0; i < std::size(trackPieceRequiredSprites); i++)
            {
                for (auto& group : trackPieceRequiredSprites[i])
                {
                    auto precision = defaultVehicle->spriteGroups[EnumValue(group.VehicleSpriteGroup)].spritePrecision;
                    if (precision < group.MinPrecision)
                        supportedPieces.set(i, false);
                }
            }
        }
        return supportedPieces;
    }

    static std::optional<int32_t> RideGetSmallestStationLength(const Ride& ride)
    {
        std::optional<int32_t> result;
        for (const auto& station : ride.getStations())
        {
            if (!station.Start.IsNull())
            {
                if (!result.has_value() || station.Length < result.value())
                {
                    result = station.Length;
                }
            }
        }
        return result;
    }

    /**
     *
     *  rct2: 0x006CB3AA
     */
    static int32_t RideGetTrackLength(const Ride& ride)
    {
        TileElement* tileElement = nullptr;
        TrackElemType trackType;
        CoordsXYZ trackStart;
        bool foundTrack = false;

        for (const auto& station : ride.getStations())
        {
            trackStart = station.GetStart();
            if (trackStart.IsNull())
                continue;

            tileElement = MapGetFirstElementAt(trackStart);
            if (tileElement == nullptr)
                continue;
            do
            {
                if (tileElement->getType() != TileElementType::track)
                    continue;

                trackType = tileElement->asTrack()->getTrackType();
                const auto& ted = GetTrackElementDescriptor(trackType);
                if (!ted.sequenceData.sequences[0].flags.has(SequenceFlag::trackOrigin))
                    continue;

                if (tileElement->getBaseZ() != trackStart.z)
                    continue;

                foundTrack = true;
            } while (!foundTrack && !(tileElement++)->isLastForTile());

            if (foundTrack)
                break;
        }

        if (!foundTrack)
            return 0;

        RideId rideIndex = tileElement->asTrack()->getRideIndex();

        auto* windowMgr = Ui::GetWindowManager();
        WindowBase* w = windowMgr->FindByClass(WindowClass::rideConstruction);
        if (w != nullptr && _rideConstructionState != RideConstructionState::state0 && _currentRideIndex == rideIndex)
        {
            RideConstructionInvalidateCurrentTrack();
        }

        bool moveSlowIt = true;
        int32_t result = 0;

        TrackCircuitIterator it;
        trackCircuitIteratorBegin(&it, { trackStart.x, trackStart.y, tileElement });

        TrackCircuitIterator slowIt = it;
        while (trackCircuitIteratorNext(&it))
        {
            trackType = it.current.element->asTrack()->getTrackType();
            const auto& ted = GetTrackElementDescriptor(trackType);
            result += ted.pieceLength;

            moveSlowIt = !moveSlowIt;
            if (moveSlowIt)
            {
                trackCircuitIteratorNext(&slowIt);
                if (trackCircuitIteratorsMatch(&it, &slowIt))
                {
                    return 0;
                }
            }
        }
        return result;
    }

    /**
     *
     *  rct2: 0x006DD57D
     */
    void Ride::updateMaxVehicles()
    {
        if (subtype == kObjectEntryIndexNull)
            return;

        const auto* rideEntry = GetRideEntryByIndex(subtype);
        if (rideEntry == nullptr)
        {
            return;
        }

        uint8_t newNumCarsPerTrain;
        int32_t maxNumTrains;

        const auto& rtd = getRideTypeDescriptor();
        if (rideEntry->cars_per_flat_ride == kNoFlatRideCars)
        {
            newNumCarsPerTrain = std::max(rideEntry->min_cars_in_train, numCarsPerTrain);
            minCarsPerTrain = rideEntry->min_cars_in_train;
            maxCarsPerTrain = rideEntry->max_cars_in_train;

            // Calculate maximum train length based on smallest station length
            auto stationNumTiles = RideGetSmallestStationLength(*this);
            if (!stationNumTiles.has_value())
                return;

            auto stationLength = (stationNumTiles.value() * 0x44180) - 0x16B2A;
            int32_t maxMass = rtd.MaxMass << 8;
            int32_t newMaxCarsPerTrain = 1;
            for (int32_t numCars = rideEntry->max_cars_in_train; numCars > 0; numCars--)
            {
                int32_t trainLength = 0;
                int32_t totalMass = 0;
                for (int32_t i = 0; i < numCars; i++)
                {
                    const auto& carEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, numCars, i)];
                    trainLength += carEntry.spacing;
                    totalMass += carEntry.carMass;
                }

                if (trainLength <= stationLength && totalMass <= maxMass)
                {
                    newMaxCarsPerTrain = numCars;
                    break;
                }
            }
            int32_t newCarsPerTrain = std::max(proposedNumCarsPerTrain, rideEntry->min_cars_in_train);
            newMaxCarsPerTrain = std::max(newMaxCarsPerTrain, static_cast<int32_t>(rideEntry->min_cars_in_train));
            if (!getGameState().cheats.disableTrainLengthLimit)
            {
                newCarsPerTrain = std::min(newMaxCarsPerTrain, newCarsPerTrain);
            }
            maxCarsPerTrain = newMaxCarsPerTrain;
            minCarsPerTrain = rideEntry->min_cars_in_train;

            switch (mode)
            {
                case RideMode::continuousCircuitBlockSectioned:
                case RideMode::poweredLaunchBlockSectioned:
                    maxNumTrains = std::clamp<int32_t>(numStations + numBlockBrakes - 1, 1, Limits::kMaxTrainsPerRide);
                    break;
                case RideMode::reverseInclineLaunchedShuttle:
                case RideMode::poweredLaunchPassthrough:
                case RideMode::shuttle:
                case RideMode::limPoweredLaunch:
                case RideMode::poweredLaunch:
                    maxNumTrains = 1;
                    break;
                default:
                    // Calculate maximum number of trains
                    int32_t trainLength = 0;
                    for (int32_t i = 0; i < newCarsPerTrain; i++)
                    {
                        const auto& carEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, newCarsPerTrain, i)];
                        trainLength += carEntry.spacing;
                    }

                    int32_t totalLength = trainLength / 2;
                    if (newCarsPerTrain != 1)
                        totalLength /= 2;

                    maxNumTrains = 0;
                    do
                    {
                        maxNumTrains++;
                        totalLength += trainLength;
                    } while (totalLength <= stationLength);

                    if ((mode != RideMode::stationToStation && mode != RideMode::continuousCircuit)
                        || !rtd.flags.has(RtdFlag::allowMoreVehiclesThanStationFits))
                    {
                        maxNumTrains = std::min(maxNumTrains, int32_t(Limits::kMaxTrainsPerRide));
                    }
                    else
                    {
                        const auto& firstCarEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, newCarsPerTrain, 0)];
                        int32_t poweredMaxSpeed = firstCarEntry.poweredMaxSpeed;

                        int32_t totalSpacing = 0;
                        for (int32_t i = 0; i < newCarsPerTrain; i++)
                        {
                            const auto& carEntry = rideEntry->Cars[RideEntryGetVehicleAtPosition(subtype, newCarsPerTrain, i)];
                            totalSpacing += carEntry.spacing;
                        }

                        totalSpacing >>= 13;
                        int32_t trackLength = RideGetTrackLength(*this) / 4;
                        if (poweredMaxSpeed > 10)
                            trackLength = (trackLength * 3) / 4;
                        if (poweredMaxSpeed > 25)
                            trackLength = (trackLength * 3) / 4;
                        if (poweredMaxSpeed > 40)
                            trackLength = (trackLength * 3) / 4;

                        maxNumTrains = 0;
                        int32_t length = 0;
                        do
                        {
                            maxNumTrains++;
                            length += totalSpacing;
                        } while (maxNumTrains < Limits::kMaxTrainsPerRide && length < trackLength);
                    }
                    break;
            }
            maxTrains = maxNumTrains;

            newNumCarsPerTrain = std::min(proposedNumCarsPerTrain, static_cast<uint8_t>(newCarsPerTrain));
        }
        else
        {
            maxTrains = rideEntry->cars_per_flat_ride;
            minCarsPerTrain = rideEntry->min_cars_in_train;
            maxCarsPerTrain = rideEntry->max_cars_in_train;
            newNumCarsPerTrain = rideEntry->max_cars_in_train;
            maxNumTrains = rideEntry->cars_per_flat_ride;
        }

        if (getGameState().cheats.disableTrainLengthLimit)
        {
            maxNumTrains = Limits::kMaxTrainsPerRide;
        }
        auto newNumTrains = std::min(proposedNumTrains, static_cast<uint8_t>(maxNumTrains));

        // Refresh new current num vehicles / num cars per vehicle
        if (newNumTrains != numTrains || newNumCarsPerTrain != numCarsPerTrain)
        {
            numCarsPerTrain = newNumCarsPerTrain;
            numTrains = newNumTrains;

            auto* windowMgr = Ui::GetWindowManager();
            windowMgr->InvalidateByNumber(WindowClass::ride, id.ToUnderlying());
        }
    }

    void Ride::updateNumberOfCircuits()
    {
        if (!canHaveMultipleCircuits())
        {
            numCircuits = 1;
        }
    }

    void Ride::setRideEntry(ObjectEntryIndex entryIndex)
    {
        auto colour = RideGetUnusedPresetVehicleColour(entryIndex, UtilRand());
        auto rideSetVehicleAction = GameActions::RideSetVehicleAction(
            id, GameActions::RideSetVehicleType::rideEntry, entryIndex, colour);
        GameActions::Execute(&rideSetVehicleAction, getGameState());
    }

    void Ride::setNumTrains(int32_t newNumTrains)
    {
        auto rideSetVehicleAction = GameActions::RideSetVehicleAction(
            id, GameActions::RideSetVehicleType::numTrains, newNumTrains);
        GameActions::Execute(&rideSetVehicleAction, getGameState());
    }

    void Ride::setNumCarsPerTrain(int32_t numCarsPerVehicle)
    {
        auto rideSetVehicleAction = GameActions::RideSetVehicleAction(
            id, GameActions::RideSetVehicleType::numCarsPerTrain, numCarsPerVehicle);
        GameActions::Execute(&rideSetVehicleAction, getGameState());
    }

    void Ride::setReversedTrains(bool reverseTrains)
    {
        auto rideSetVehicleAction = GameActions::RideSetVehicleAction(
            id, GameActions::RideSetVehicleType::trainsReversed, reverseTrains);
        GameActions::Execute(&rideSetVehicleAction, getGameState());
    }

    /**
     *
     *  rct2: 0x006B752C
     */
    void Ride::crash(uint8_t vehicleIndex)
    {
        Vehicle* vehicle = getGameState().entities.getEntity<Vehicle>(vehicles[vehicleIndex]);

        if (gLegacyScene != LegacyScene::titleSequence && vehicle != nullptr)
        {
            // Open ride window for crashed vehicle
            auto intent = Intent(WindowDetail::vehicle);
            intent.PutExtra(INTENT_EXTRA_VEHICLE, vehicle);
            WindowBase* w = ContextOpenIntent(&intent);

            Viewport* viewport = WindowGetViewport(w);
            if (w != nullptr && viewport != nullptr)
            {
                viewport->flags |= VIEWPORT_FLAG_SOUND_ON;
            }
        }

        if (Config::Get().notifications.rideCrashed)
        {
            Formatter ft;
            formatNameTo(ft);
            News::AddItemToQueue(News::ItemType::ride, STR_RIDE_HAS_CRASHED, id.ToUnderlying(), ft);
        }
    }

    // Gets the approximate value of customers per hour for this ride. Multiplies ride_customers_in_last_5_minutes() by 12.
    uint32_t RideCustomersPerHour(const Ride& ride)
    {
        return RideCustomersInLast5Minutes(ride) * 12;
    }

    // Calculates the number of customers for this ride in the last 5 minutes (or more correctly 9600 game ticks)
    uint32_t RideCustomersInLast5Minutes(const Ride& ride)
    {
        uint32_t sum = 0;

        for (int32_t i = 0; i < Limits::kCustomerHistorySize; i++)
        {
            sum += ride.numCustomers[i];
        }

        return sum;
    }

    Vehicle* RideGetBrokenVehicle(const Ride& ride)
    {
        auto vehicleIndex = ride.vehicles[ride.brokenTrain];
        Vehicle* vehicle = getGameState().entities.getEntity<Vehicle>(vehicleIndex);
        if (vehicle != nullptr)
        {
            return vehicle->GetCar(ride.brokenCar);
        }
        return nullptr;
    }

    /**
     *
     *  rct2: 0x006D235B
     */
    void Ride::remove()
    {
        RideDelete(id);
    }

    void Ride::renew()
    {
        // Set build date to current date (so the ride is brand new)
        buildDate = GetDate().GetMonthsElapsed();
        reliability = kRideInitialReliability;
        std::fill(std::begin(downtimeHistory), std::end(downtimeHistory), 0);
        downtime = 0;
    }

    RideClassification Ride::getClassification() const
    {
        const auto& rtd = getRideTypeDescriptor();
        return rtd.Classification;
    }

    bool Ride::isRide() const
    {
        return getClassification() == RideClassification::ride;
    }

    money64 RideGetPrice(const Ride& ride)
    {
        auto& park = getGameState().park;
        if (park.flags.has(ParkFlag::noMoney))
            return 0;
        if (ride.isRide())
        {
            if (!Park::RidePricesUnlocked(park))
            {
                return 0;
            }
        }
        return ride.price[0];
    }

    /**
     * Return the tile_element of an adjacent station at x,y,z(+-2).
     * Returns nullptr if no suitable tile_element is found.
     */
    TileElement* GetStationPlatform(const CoordsXYRangedZ& coords)
    {
        bool foundTileElement = false;
        TileElement* tileElement = MapGetFirstElementAt(coords);
        if (tileElement != nullptr)
        {
            do
            {
                if (tileElement->getType() != TileElementType::track)
                    continue;
                /* Check if tileElement is a station platform. */
                if (!tileElement->asTrack()->isStation())
                    continue;

                if (coords.baseZ > tileElement->getBaseZ() || coords.clearanceZ < tileElement->getBaseZ())
                {
                    /* The base height of tileElement is not within
                     * the z tolerance. */
                    continue;
                }

                foundTileElement = true;
                break;
            } while (!(tileElement++)->isLastForTile());
        }
        if (!foundTileElement)
        {
            return nullptr;
        }

        return tileElement;
    }

    /**
     * Check for an adjacent station to x,y,z in direction.
     */
    static bool CheckForAdjacentStation(const CoordsXYZ& stationCoords, uint8_t direction)
    {
        bool found = false;
        int32_t adjX = stationCoords.x;
        int32_t adjY = stationCoords.y;
        for (uint32_t i = 0; i <= kRideAdjacencyCheckDistance; i++)
        {
            adjX += CoordsDirectionDelta[direction].x;
            adjY += CoordsDirectionDelta[direction].y;
            TileElement* stationElement = GetStationPlatform(
                { { adjX, adjY, stationCoords.z - 2 * kCoordsZStep }, stationCoords.z + 2 * kCoordsZStep });
            if (stationElement != nullptr)
            {
                auto rideIndex = stationElement->asTrack()->getRideIndex();
                auto ride = GetRide(rideIndex);
                if (ride != nullptr && (ride->departFlags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS))
                {
                    found = true;
                }
            }
        }
        return found;
    }

    /**
     * Return whether ride has at least one adjacent station to it.
     */
    bool RideHasAdjacentStation(const Ride& ride)
    {
        bool found = false;

        /* Loop through all of the ride stations, checking for an
         * adjacent station on either side. */
        for (const auto& station : ride.getStations())
        {
            auto stationStart = station.GetStart();
            if (!stationStart.IsNull())
            {
                /* Get the map element for the station start. */
                TileElement* stationElement = GetStationPlatform({ stationStart, stationStart.z + 0 });
                if (stationElement == nullptr)
                {
                    continue;
                }
                /* Check the first side of the station */
                int32_t direction = stationElement->getDirectionWithOffset(1);
                found = CheckForAdjacentStation(stationStart, direction);
                if (found)
                    break;
                /* Check the other side of the station */
                direction = DirectionReverse(direction);
                found = CheckForAdjacentStation(stationStart, direction);
                if (found)
                    break;
            }
        }
        return found;
    }

    bool RideHasStationShelter(const Ride& ride)
    {
        const auto* stationObj = ride.getStationObject();
        return stationObj != nullptr && stationObj->Flags.has(StationObjectFlag::hasShelter);
    }

    bool RideHasRatings(const Ride& ride)
    {
        return !ride.ratings.isNull();
    }

    int32_t GetUnifiedBoosterSpeed(ride_type_t rideType, int32_t relativeSpeed)
    {
        return GetRideTypeDescriptor(rideType).GetUnifiedBoosterSpeed(relativeSpeed);
    }

    void FixInvalidVehicleSpriteSizes()
    {
        auto& gameState = getGameState();
        for (const auto& ride : RideManager(gameState))
        {
            for (auto entityIndex : ride.vehicles)
            {
                for (Vehicle* vehicle = getGameState().entities.tryGetEntity<Vehicle>(entityIndex); vehicle != nullptr;
                     vehicle = getGameState().entities.tryGetEntity<Vehicle>(vehicle->next_vehicle_on_train))
                {
                    auto carEntry = vehicle->Entry();
                    if (carEntry == nullptr)
                    {
                        break;
                    }

                    if (vehicle->spriteData.width == 0)
                    {
                        vehicle->spriteData.width = carEntry->spriteWidth;
                    }
                    if (vehicle->spriteData.heightMin == 0)
                    {
                        vehicle->spriteData.heightMin = carEntry->spriteHeightNegative;
                    }
                    if (vehicle->spriteData.heightMax == 0)
                    {
                        vehicle->spriteData.heightMax = carEntry->spriteHeightPositive;
                    }
                }
            }
        }
    }

    bool RideEntryHasCategory(const RideObjectEntry& rideEntry, RideCategory category)
    {
        auto rideType = rideEntry.GetFirstNonNullRideType();
        return GetRideTypeDescriptor(rideType).Category == category;
    }

    ObjectEntryIndex RideGetEntryIndex(ride_type_t rideType, ObjectEntryIndex rideSubType)
    {
        auto subType = rideSubType;

        if (subType == kObjectEntryIndexNull)
        {
            auto& objManager = GetContext()->GetObjectManager();
            auto& rideEntries = objManager.GetAllRideEntries(rideType);
            if (!rideEntries.empty())
            {
                subType = rideEntries[0];
                for (auto rideEntryIndex : rideEntries)
                {
                    const auto* rideEntry = GetRideEntryByIndex(rideEntryIndex);
                    if (rideEntry == nullptr)
                    {
                        return kObjectEntryIndexNull;
                    }

                    // Can happen in select-by-track-type mode
                    if (!RideEntryIsInvented(rideEntryIndex) && !getGameState().cheats.ignoreResearchStatus)
                    {
                        continue;
                    }

                    if (!GetRideTypeDescriptor(rideType).flags.has(RtdFlag::listVehiclesSeparately))
                    {
                        subType = rideEntryIndex;
                        break;
                    }
                }
            }
        }

        return subType;
    }

    const StationObject* Ride::getStationObject() const
    {
        auto& objManager = GetContext()->GetObjectManager();
        return objManager.GetLoadedObject<StationObject>(entranceStyle);
    }

    const MusicObject* Ride::getMusicObject() const
    {
        auto& objManager = GetContext()->GetObjectManager();
        return objManager.GetLoadedObject<MusicObject>(music);
    }

    // Normally, a station has at most one entrance and one exit, which are at the same height
    // as the station. But in hacked parks, neither can be taken for granted. This code ensures
    // that the ride.entrances and ride.exits arrays will point to one of them. There is
    // an ever-so-slight chance two entrances/exits for the same station reside on the same tile.
    // In cases like this, the one at station height will be considered the "true" one.
    // If none exists at that height, newer and higher placed ones take precedence.
    void DetermineRideEntranceAndExitLocations()
    {
        LOG_VERBOSE("Inspecting ride entrance / exit locations");

        auto& gameState = getGameState();
        for (auto& ride : RideManager(gameState))
        {
            for (auto& station : ride.getStations())
            {
                auto stationIndex = ride.getStationIndex(&station);
                TileCoordsXYZD entranceLoc = station.Entrance;
                TileCoordsXYZD exitLoc = station.Exit;
                bool fixEntrance = false;
                bool fixExit = false;

                // Skip if the station has no entrance
                if (!entranceLoc.IsNull())
                {
                    const EntranceElement* entranceElement = MapGetRideEntranceElementAt(entranceLoc.ToCoordsXYZD(), false);

                    if (entranceElement == nullptr || entranceElement->getRideIndex() != ride.id
                        || entranceElement->getStationIndex() != stationIndex)
                    {
                        fixEntrance = true;
                    }
                    else
                    {
                        station.Entrance.direction = entranceElement->getDirection();
                    }
                }

                if (!exitLoc.IsNull())
                {
                    const EntranceElement* entranceElement = MapGetRideExitElementAt(exitLoc.ToCoordsXYZD(), false);

                    if (entranceElement == nullptr || entranceElement->getRideIndex() != ride.id
                        || entranceElement->getStationIndex() != stationIndex)
                    {
                        fixExit = true;
                    }
                    else
                    {
                        station.Exit.direction = 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 y = 1; y < gameState.mapSize.y - 1; y++)
                {
                    for (int32_t x = 1; x < gameState.mapSize.x - 1; x++)
                    {
                        TileElement* tileElement = MapGetFirstElementAt(TileCoordsXY{ x, y });

                        if (tileElement != nullptr)
                        {
                            do
                            {
                                if (tileElement->getType() != TileElementType::entrance)
                                {
                                    continue;
                                }
                                const EntranceElement* entranceElement = tileElement->asEntrance();
                                if (entranceElement->getRideIndex() != ride.id)
                                {
                                    continue;
                                }
                                if (entranceElement->getStationIndex() != stationIndex)
                                {
                                    continue;
                                }

                                // The expected height is where entrances and exit reside in non-hacked parks.
                                const uint8_t expectedHeight = station.Height;

                                if (fixEntrance && entranceElement->getEntranceType() == EntranceType::rideEntrance)
                                {
                                    if (alreadyFoundEntrance)
                                    {
                                        if (station.Entrance.z == expectedHeight)
                                            continue;
                                        if (station.Entrance.z > entranceElement->baseHeight)
                                            continue;
                                    }

                                    // Found our entrance
                                    station.Entrance = { x, y, entranceElement->baseHeight, entranceElement->getDirection() };
                                    alreadyFoundEntrance = true;

                                    LOG_VERBOSE(
                                        "Fixed disconnected entrance of ride %d, station %d to x = %d, y = %d and z = %d.",
                                        ride.id, stationIndex, x, y, entranceElement->baseHeight);
                                }
                                else if (fixExit && entranceElement->getEntranceType() == EntranceType::rideExit)
                                {
                                    if (alreadyFoundExit)
                                    {
                                        if (station.Exit.z == expectedHeight)
                                            continue;
                                        if (station.Exit.z > entranceElement->baseHeight)
                                            continue;
                                    }

                                    // Found our exit
                                    station.Exit = { x, y, entranceElement->baseHeight, entranceElement->getDirection() };
                                    alreadyFoundExit = true;

                                    LOG_VERBOSE(
                                        "Fixed disconnected exit of ride %d, station %d to x = %d, y = %d and z = %d.", ride.id,
                                        stationIndex, x, y, entranceElement->baseHeight);
                                }
                            } while (!(tileElement++)->isLastForTile());
                        }
                    }
                }

                if (fixEntrance && !alreadyFoundEntrance)
                {
                    station.Entrance.SetNull();
                    LOG_VERBOSE("Cleared disconnected entrance of ride %d, station %d.", ride.id, stationIndex);
                }
                if (fixExit && !alreadyFoundExit)
                {
                    station.Exit.SetNull();
                    LOG_VERBOSE("Cleared disconnected exit of ride %d, station %d.", ride.id, stationIndex);
                }
            }
        }
    }

    void RideClearLeftoverEntrances(const Ride& ride)
    {
        auto& gameState = getGameState();
        for (TileCoordsXY tilePos = {}; tilePos.x < gameState.mapSize.x; ++tilePos.x)
        {
            for (tilePos.y = 0; tilePos.y < gameState.mapSize.y; ++tilePos.y)
            {
                for (auto* entrance : TileElementsView<EntranceElement>(tilePos.ToCoordsXY()))
                {
                    const bool isRideEntranceExit = entrance->getEntranceType() == EntranceType::rideEntrance
                        || entrance->getEntranceType() == EntranceType::rideExit;
                    if (!isRideEntranceExit)
                        continue;
                    if (entrance->getRideIndex() != ride.id)
                        continue;

                    TileElementRemove(entrance->as<TileElement>());
                }
            }
        }
    }

    std::string Ride::getName() const
    {
        Formatter ft;
        formatNameTo(ft);
        return FormatStringIDLegacy(STR_STRINGID, reinterpret_cast<const void*>(ft.Data()));
    }

    void Ride::formatNameTo(Formatter& ft) const
    {
        if (!customName.empty())
        {
            auto str = customName.c_str();
            ft.Add<StringId>(STR_STRING);
            ft.Add<const char*>(str);
        }
        else
        {
            const auto rideTypeName = getTypeNaming().Name;
            ft.Add<StringId>(1).Add<StringId>(rideTypeName).Add<uint16_t>(defaultNameNumber);
        }
    }

    uint64_t Ride::getAvailableModes() const
    {
        if (getGameState().cheats.showAllOperatingModes)
            return kAllRideModesAvailable;

        return getRideTypeDescriptor().RideModes;
    }

    const RideTypeDescriptor& Ride::getRideTypeDescriptor() const
    {
        return GetRideTypeDescriptor(type);
    }

    RideNaming Ride::getTypeNaming() const
    {
        return GetRideNaming(type, getRideEntry());
    }

    uint8_t Ride::getNumShelteredSections() const
    {
        return numShelteredSections & ShelteredSectionsBits::kNumShelteredSectionsMask;
    }

    void Ride::increaseNumShelteredSections()
    {
        auto newNumShelteredSections = getNumShelteredSections();
        if (newNumShelteredSections != 0x1F)
            newNumShelteredSections++;
        numShelteredSections &= ~ShelteredSectionsBits::kNumShelteredSectionsMask;
        numShelteredSections |= newNumShelteredSections;
    }

    void Ride::updateRideTypeForAllPieces()
    {
        auto& gameState = getGameState();
        for (int32_t y = 0; y < gameState.mapSize.y; y++)
        {
            for (int32_t x = 0; x < gameState.mapSize.x; x++)
            {
                auto* tileElement = MapGetFirstElementAt(TileCoordsXY(x, y));
                if (tileElement == nullptr)
                    continue;

                do
                {
                    if (tileElement->getType() != TileElementType::track)
                        continue;

                    auto* trackElement = tileElement->asTrack();
                    if (trackElement->getRideIndex() != id)
                        continue;

                    trackElement->setRideType(type);

                } while (!(tileElement++)->isLastForTile());
            }
        }
    }

    bool Ride::hasRecolourableShopItems() const
    {
        const auto rideEntry = getRideEntry();
        if (rideEntry == nullptr)
            return false;

        for (size_t itemIndex = 0; itemIndex < std::size(rideEntry->shop_item); itemIndex++)
        {
            const ShopItem currentItem = rideEntry->shop_item[itemIndex];
            if (currentItem != ShopItem::none && GetShopItemDescriptor(currentItem).IsRecolourable())
            {
                return true;
            }
        }
        return false;
    }

    bool Ride::hasStation() const
    {
        return numStations != 0;
    }

    std::vector<RideId> GetTracklessRides()
    {
        // Iterate map and build list of seen ride IDs
        std::vector<bool> seen;
        seen.resize(256);
        TileElementIterator it;
        TileElementIteratorBegin(&it);
        while (TileElementIteratorNext(&it))
        {
            auto trackEl = it.element->asTrack();
            if (trackEl != nullptr && !trackEl->isGhost())
            {
                auto rideId = trackEl->getRideIndex().ToUnderlying();
                if (rideId >= seen.size())
                {
                    seen.resize(rideId + 1);
                }
                seen[rideId] = true;
            }
        }

        // Get all rides that did not get seen during map iteration
        auto& gameState = getGameState();
        const auto& rideManager = RideManager(gameState);
        std::vector<RideId> result;
        for (const auto& ride : rideManager)
        {
            const auto rideIndex = ride.id.ToUnderlying();
            if (seen.size() <= rideIndex || !seen[rideIndex])
            {
                result.push_back(ride.id);
            }
        }
        return result;
    }

    ResultWithMessage Ride::changeStatusDoStationChecks(StationIndex& stationIndex)
    {
        auto stationIndexCheck = RideModeCheckStationPresent(*this);
        stationIndex = stationIndexCheck.StationIndex;
        if (stationIndex.IsNull())
            return { false, stationIndexCheck.Message };

        auto stationNumbersCheck = RideModeCheckValidStationNumbers(*this);
        if (!stationNumbersCheck.Successful)
            return { false, stationNumbersCheck.Message };

        return { true };
    }

    ResultWithMessage Ride::changeStatusGetStartElement(StationIndex stationIndex, CoordsXYE& trackElement)
    {
        auto startLoc = getStation(stationIndex).Start;
        trackElement.x = startLoc.x;
        trackElement.y = startLoc.y;
        trackElement.element = reinterpret_cast<TileElement*>(getOriginElement(stationIndex));
        if (trackElement.element == nullptr)
        {
            // Maze is strange, station start is 0... investigation required
            const auto& rtd = getRideTypeDescriptor();
            if (rtd.specialType != RtdSpecialType::maze)
                return { false };
        }

        return { true };
    }

    ResultWithMessage Ride::changeStatusCheckCompleteCircuit(const CoordsXYE& trackElement)
    {
        CoordsXYE problematicTrackElement = {};
        if (mode == RideMode::race || mode == RideMode::continuousCircuit || isBlockSectioned())
        {
            if (findTrackGap(*this, trackElement, &problematicTrackElement))
            {
                RideScrollToTrackError(problematicTrackElement);
                return { false, STR_TRACK_IS_NOT_A_COMPLETE_CIRCUIT };
            }
        }

        return { true };
    }

    ResultWithMessage Ride::changeStatusCheckTrackValidity(const CoordsXYE& trackElement, bool isSimulating)
    {
        CoordsXYE problematicTrackElement = {};

        if (isBlockSectioned())
        {
            auto blockBrakeCheck = RideCheckBlockBrakes(trackElement, &problematicTrackElement, !isSimulating);
            if (!blockBrakeCheck.Successful)
            {
                RideScrollToTrackError(problematicTrackElement);
                return { false, blockBrakeCheck.Message };
            }
        }

        if (subtype != kObjectEntryIndexNull && !getGameState().cheats.enableAllDrawableTrackPieces)
        {
            const auto* rideEntry = GetRideEntryByIndex(subtype);
            if (rideEntry == nullptr)
            {
                return { false, STR_UNKNOWN_RIDE };
            }
            if (rideEntry->flags.has(RideEntryFlag::noInversions))
            {
                if (RideCheckTrackContainsInversions(trackElement, &problematicTrackElement))
                {
                    RideScrollToTrackError(problematicTrackElement);
                    return { false, STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN };
                }
            }
            if (rideEntry->flags.has(RideEntryFlag::noBankedTrack))
            {
                if (RideCheckTrackContainsBanked(trackElement, &problematicTrackElement))
                {
                    RideScrollToTrackError(problematicTrackElement);
                    return { false, STR_TRACK_UNSUITABLE_FOR_TYPE_OF_TRAIN };
                }
            }
        }

        if (mode == RideMode::stationToStation)
        {
            if (!findTrackGap(*this, trackElement, &problematicTrackElement))
            {
                return { false, STR_RIDE_MUST_START_AND_END_WITH_STATIONS };
            }

            if (!RideCheckStationLength(trackElement, &problematicTrackElement))
            {
                RideScrollToTrackError(problematicTrackElement);
                return { false, STR_STATION_NOT_LONG_ENOUGH };
            }

            if (!RideCheckStartAndEndIsStation(trackElement))
            {
                RideScrollToTrackError(problematicTrackElement);
                return { false, STR_RIDE_MUST_START_AND_END_WITH_STATIONS };
            }
        }

        return { true };
    }

    ResultWithMessage Ride::changeStatusCreateVehicles(bool isApplying, const CoordsXYE& trackElement, bool isSimulating)
    {
        if (isApplying)
            RideSetStartFinishPoints(id, trackElement);

        const auto& rtd = getRideTypeDescriptor();
        if (!rtd.flags.has(RtdFlag::noVehicles) && !flags.has(RideFlag::onTrack))
        {
            const auto createVehicleResult = createVehicles(trackElement, isApplying, isSimulating);
            if (!createVehicleResult.Successful)
            {
                return { false, createVehicleResult.Message };
            }
        }

        if (rtd.flags.has(RtdFlag::allowCableLiftHill) && flags.has(RideFlag::cableLiftHillComponentUsed)
            && !flags.has(RideFlag::cableLift))
        {
            const auto createCableLiftResult = RideCreateCableLift(id, isApplying);
            if (!createCableLiftResult.Successful)
                return { false, createCableLiftResult.Message };
        }

        return { true };
    }

    RideMode RideModeGetBlockSectionedCounterpart(RideMode originalMode)
    {
        assert(originalMode < RideMode::count);
        return kRideModeBlockSectionedCounterpart[EnumValue(originalMode)];
    }
} // namespace OpenRCT2
