/*****************************************************************************
 * Copyright (c) 2014-2025 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 "RideRatings.h"

#include "../Cheats.h"
#include "../Context.h"
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../core/UnitConversion.h"
#include "../profiling/Profiling.h"
#include "../scripting/ScriptEngine.h"
#include "../ui/WindowManager.h"
#include "../world/Map.h"
#include "../world/tile_element/PathElement.h"
#include "../world/tile_element/SurfaceElement.h"
#include "../world/tile_element/TileElement.h"
#include "../world/tile_element/TrackElement.h"
#include "Ride.h"
#include "RideData.h"
#include "RideManager.hpp"
#include "Station.h"
#include "Track.h"
#include "TrackData.h"

#include <iterator>

using namespace OpenRCT2;
using namespace OpenRCT2::Scripting;
using namespace OpenRCT2::TrackMetaData;

enum
{
    RIDE_RATINGS_STATE_FIND_NEXT_RIDE,
    RIDE_RATINGS_STATE_INITIALISE,
    RIDE_RATINGS_STATE_2,
    RIDE_RATINGS_STATE_CALCULATE,
    RIDE_RATINGS_STATE_4,
    RIDE_RATINGS_STATE_5
};

enum
{
    PROXIMITY_WATER_OVER,                   // 0x0138B596
    PROXIMITY_WATER_TOUCH,                  // 0x0138B598
    PROXIMITY_WATER_LOW,                    // 0x0138B59A
    PROXIMITY_WATER_HIGH,                   // 0x0138B59C
    PROXIMITY_SURFACE_TOUCH,                // 0x0138B59E
    PROXIMITY_QUEUE_PATH_OVER,              // 0x0138B5A0
    PROXIMITY_QUEUE_PATH_TOUCH_ABOVE,       // 0x0138B5A2
    PROXIMITY_QUEUE_PATH_TOUCH_UNDER,       // 0x0138B5A4
    PROXIMITY_PATH_TOUCH_ABOVE,             // 0x0138B5A6
    PROXIMITY_PATH_TOUCH_UNDER,             // 0x0138B5A8
    PROXIMITY_OWN_TRACK_TOUCH_ABOVE,        // 0x0138B5AA
    PROXIMITY_OWN_TRACK_CLOSE_ABOVE,        // 0x0138B5AC
    PROXIMITY_FOREIGN_TRACK_ABOVE_OR_BELOW, // 0x0138B5AE
    PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE,    // 0x0138B5B0
    PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE,    // 0x0138B5B2
    PROXIMITY_SCENERY_SIDE_BELOW,           // 0x0138B5B4
    PROXIMITY_SCENERY_SIDE_ABOVE,           // 0x0138B5B6
    PROXIMITY_OWN_STATION_TOUCH_ABOVE,      // 0x0138B5B8
    PROXIMITY_OWN_STATION_CLOSE_ABOVE,      // 0x0138B5BA
    PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP,  // 0x0138B5BC
    PROXIMITY_PATH_TROUGH_VERTICAL_LOOP,    // 0x0138B5BE
    PROXIMITY_INTERSECTING_VERTICAL_LOOP,   // 0x0138B5C0
    PROXIMITY_THROUGH_VERTICAL_LOOP,        // 0x0138B5C2
    PROXIMITY_PATH_SIDE_CLOSE,              // 0x0138B5C4
    PROXIMITY_FOREIGN_TRACK_SIDE_CLOSE,     // 0x0138B5C6
    PROXIMITY_SURFACE_SIDE_CLOSE,           // 0x0138B5C8
    PROXIMITY_COUNT
};

struct ShelteredEights
{
    uint8_t TrackShelteredEighths;
    uint8_t TotalShelteredEighths;
};

// Amount of updates allowed per updating state on the current tick.
// The total amount would be MaxRideRatingSubSteps * kRideRatingMaxUpdateStates which
// would be currently 80, this is the worst case of sub-steps and may break out earlier.
static constexpr size_t MaxRideRatingUpdateSubSteps = 20;

static void ride_ratings_update_state(RideRatingUpdateState& state);
static void ride_ratings_update_state_0(RideRatingUpdateState& state);
static void ride_ratings_update_state_1(RideRatingUpdateState& state);
static void ride_ratings_update_state_2(RideRatingUpdateState& state);
static void ride_ratings_update_state_3(RideRatingUpdateState& state);
static void ride_ratings_update_state_4(RideRatingUpdateState& state);
static void ride_ratings_update_state_5(RideRatingUpdateState& state);
static void ride_ratings_begin_proximity_loop(RideRatingUpdateState& state);
static void RideRatingsCalculate(RideRatingUpdateState& state, Ride& ride);
static void RideRatingsCalculateValue(Ride& ride);
static void ride_ratings_score_close_proximity(RideRatingUpdateState& state, TileElement* inputTileElement);
static void RideRatingsAdd(RatingTuple& ratings, int32_t excitement, int32_t intensity, int32_t nausea);

static ShelteredEights GetNumOfShelteredEighths(const Ride& ride);
static money64 RideComputeUpkeep(RideRatingUpdateState& state, const Ride& ride);
static void SetUnreliabilityFactor(Ride& ride);

static void RideRatingsApplyAdjustments(const Ride& ride, RatingTuple& ratings);
static void RideRatingsApplyIntensityPenalty(RatingTuple& ratings);

static void RideRatingsApplyBonusLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusSynchronisation(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusTrainLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusAverageSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusDuration(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusGForces(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusTurns(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusSheltered(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusRotations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusOperationOption(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusReversedTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusGoKartRace(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusTowerRide(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusRotoDrop(RatingTuple& ratings, const Ride& ride);
static void RideRatingsApplyBonusMazeSize(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusBoatHireNoCircuit(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusSlideUnlimitedRides(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusMotionSimulatorMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonus3DCinemaMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusTopSpinMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusReversals(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
static void RideRatingsApplyBonusHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusNumTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusDownwardLaunch(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyBonusLaunchedFreefallSpecial(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
static void RideRatingsApplyBonusProximity(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
static void RideRatingsApplyBonusScenery(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementDropHeight(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementNumDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementNegativeGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementInversions(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementUnsheltered(
    RatingTuple& ratings, const Ride& ride, uint8_t shelteredEighths, RatingsModifier modifier);
static void RideRatingsApplyRequirementReversals(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
static void RideRatingsApplyRequirementHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementStations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyRequirementSplashdown(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
static void RideRatingsApplyPenaltyLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);

void RideRatingResetUpdateStates()
{
    RideRatingUpdateState nullState{};
    nullState.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;

    auto& updateStates = GetGameState().RideRatingUpdateStates;
    std::fill(updateStates.begin(), updateStates.end(), nullState);
}

/**
 * This is a small hack function to keep calling the ride rating processor until
 * the given ride's ratings have been calculated. Whatever is currently being
 * processed will be overwritten.
 * Only purpose of this function currently is for testing.
 */
void RideRatingsUpdateRide(const Ride& ride)
{
    RideRatingUpdateState state;
    if (ride.status != RideStatus::Closed)
    {
        state.CurrentRide = ride.id;
        state.State = RIDE_RATINGS_STATE_INITIALISE;
        while (state.State != RIDE_RATINGS_STATE_FIND_NEXT_RIDE)
        {
            ride_ratings_update_state(state);
        }
    }
}

/**
 *
 *  rct2: 0x006B5A2A
 */
void RideRatingsUpdateAll()
{
    PROFILED_FUNCTION();

    if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR)
        return;

    for (auto& updateState : GetGameState().RideRatingUpdateStates)
    {
        for (size_t i = 0; i < MaxRideRatingUpdateSubSteps; ++i)
        {
            ride_ratings_update_state(updateState);

            // We need to abort the loop if the state machine requested to find the next ride.
            if (updateState.State == RIDE_RATINGS_STATE_FIND_NEXT_RIDE)
                break;
        }
    }
}

static void ride_ratings_update_state(RideRatingUpdateState& state)
{
    switch (state.State)
    {
        case RIDE_RATINGS_STATE_FIND_NEXT_RIDE:
            ride_ratings_update_state_0(state);
            break;
        case RIDE_RATINGS_STATE_INITIALISE:
            ride_ratings_update_state_1(state);
            break;
        case RIDE_RATINGS_STATE_2:
            ride_ratings_update_state_2(state);
            break;
        case RIDE_RATINGS_STATE_CALCULATE:
            ride_ratings_update_state_3(state);
            break;
        case RIDE_RATINGS_STATE_4:
            ride_ratings_update_state_4(state);
            break;
        case RIDE_RATINGS_STATE_5:
            ride_ratings_update_state_5(state);
            break;
    }
}

static bool RideRatingIsUpdatingRide(RideId id)
{
    const auto& updateStates = GetGameState().RideRatingUpdateStates;
    return std::any_of(updateStates.begin(), updateStates.end(), [id](auto& state) {
        return state.CurrentRide == id && state.State != RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
    });
}

static bool ShouldSkipRatingCalculation(const Ride& ride)
{
    // Skip rides that are closed.
    if (ride.status == RideStatus::Closed)
    {
        return true;
    }

    // Skip anything that is already updating.
    if (RideRatingIsUpdatingRide(ride.id))
    {
        return true;
    }

    // Skip rides that have a fixed rating.
    if (ride.lifecycle_flags & RIDE_LIFECYCLE_FIXED_RATINGS)
    {
        return true;
    }

    return false;
}

static RideId GetNextRideToUpdate(RideId currentRide)
{
    auto rm = GetRideManager();
    if (rm.size() == 0)
    {
        return RideId::GetNull();
    }

    auto it = rm.get(currentRide);
    if (it == rm.end())
    {
        // Start at the beginning, ride is missing.
        it = rm.begin();
    }
    else
    {
        it = std::next(it);
    }

    // Filter out rides to avoid wasting a tick to find the next ride.
    while (it != rm.end() && ShouldSkipRatingCalculation(*it))
    {
        it++;
    }

    // If we reached the end of the list we start over,
    // in case the next ride doesn't pass the filter function it will
    // look for the next matching ride in the next tick.
    if (it == rm.end())
        it = rm.begin();

    return (*it).id;
}

/**
 *
 *  rct2: 0x006B5A5C
 */
static void ride_ratings_update_state_0(RideRatingUpdateState& state)
{
    // It is possible that the current ride being calculated has
    // been removed or due to import invalid. For both, reset
    // ratings and start check at the start
    if (GetRide(state.CurrentRide) == nullptr)
    {
        state.CurrentRide = {};
    }

    const auto nextRideId = GetNextRideToUpdate(state.CurrentRide);
    const auto* nextRide = GetRide(nextRideId);
    if (nextRide != nullptr && !ShouldSkipRatingCalculation(*nextRide))
    {
        Guard::Assert(!RideRatingIsUpdatingRide(nextRideId));
        state.State = RIDE_RATINGS_STATE_INITIALISE;
    }
    state.CurrentRide = nextRideId;
}

/**
 *
 *  rct2: 0x006B5A94
 */
static void ride_ratings_update_state_1(RideRatingUpdateState& state)
{
    state.ProximityTotal = 0;
    for (int32_t i = 0; i < PROXIMITY_COUNT; i++)
    {
        state.ProximityScores[i] = 0;
    }
    state.AmountOfBrakes = 0;
    state.AmountOfReversers = 0;
    state.State = RIDE_RATINGS_STATE_2;
    state.StationFlags = 0;
    ride_ratings_begin_proximity_loop(state);
}

/**
 *
 *  rct2: 0x006B5C66
 */
static void ride_ratings_update_state_2(RideRatingUpdateState& state)
{
    const RideId rideIndex = state.CurrentRide;
    auto ride = GetRide(rideIndex);
    if (ride == nullptr || ride->status == RideStatus::Closed || ride->type >= RIDE_TYPE_COUNT)
    {
        state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
        return;
    }

    auto loc = state.Proximity;
    OpenRCT2::TrackElemType trackType = state.ProximityTrackType;

    TileElement* tileElement = MapGetFirstElementAt(loc);
    if (tileElement == nullptr)
    {
        state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
        return;
    }
    do
    {
        if (tileElement->IsGhost())
            continue;
        if (tileElement->GetType() != TileElementType::Track)
            continue;
        if (tileElement->GetBaseZ() != loc.z)
            continue;
        if (tileElement->AsTrack()->GetRideIndex() != ride->id)
        {
            // Only check that the track belongs to the same ride if ride does not have buildable track
            if (!ride->GetRideTypeDescriptor().HasFlag(RtdFlag::hasTrack))
                continue;
        }

        if (trackType == TrackElemType::None
            || (tileElement->AsTrack()->GetSequenceIndex() == 0 && trackType == tileElement->AsTrack()->GetTrackType()))
        {
            if (trackType == TrackElemType::EndStation)
            {
                auto entranceIndex = tileElement->AsTrack()->GetStationIndex();
                state.StationFlags &= ~RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
                if (ride->GetStation(entranceIndex).Entrance.IsNull())
                {
                    state.StationFlags |= RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
                }
            }

            ride_ratings_score_close_proximity(state, tileElement);

            CoordsXYE trackElement = { state.Proximity, tileElement };
            CoordsXYE nextTrackElement;
            if (!TrackBlockGetNext(&trackElement, &nextTrackElement, nullptr, nullptr))
            {
                state.State = RIDE_RATINGS_STATE_4;
                return;
            }

            loc = { nextTrackElement, nextTrackElement.element->GetBaseZ() };
            tileElement = nextTrackElement.element;
            if (loc == state.ProximityStart)
            {
                state.State = RIDE_RATINGS_STATE_CALCULATE;
                return;
            }
            state.Proximity = loc;
            state.ProximityTrackType = tileElement->AsTrack()->GetTrackType();
            return;
        }
    } while (!(tileElement++)->IsLastForTile());

    state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
}

/**
 *
 *  rct2: 0x006B5E4D
 */
static void ride_ratings_update_state_3(RideRatingUpdateState& state)
{
    auto ride = GetRide(state.CurrentRide);
    if (ride == nullptr || ride->status == RideStatus::Closed)
    {
        state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
        return;
    }

    RideRatingsCalculate(state, *ride);
    RideRatingsCalculateValue(*ride);

    auto* windowMgr = Ui::GetWindowManager();
    windowMgr->InvalidateByNumber(WindowClass::Ride, state.CurrentRide.ToUnderlying());

    state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
}

/**
 *
 *  rct2: 0x006B5BAB
 */
static void ride_ratings_update_state_4(RideRatingUpdateState& state)
{
    state.State = RIDE_RATINGS_STATE_5;
    ride_ratings_begin_proximity_loop(state);
}

/**
 *
 *  rct2: 0x006B5D72
 */
static void ride_ratings_update_state_5(RideRatingUpdateState& state)
{
    auto ride = GetRide(state.CurrentRide);
    if (ride == nullptr || ride->status == RideStatus::Closed)
    {
        state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
        return;
    }

    auto loc = state.Proximity;
    OpenRCT2::TrackElemType trackType = state.ProximityTrackType;

    TileElement* tileElement = MapGetFirstElementAt(loc);
    if (tileElement == nullptr)
    {
        state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
        return;
    }
    do
    {
        if (tileElement->IsGhost())
            continue;
        if (tileElement->GetType() != TileElementType::Track)
            continue;
        if (tileElement->GetBaseZ() != loc.z)
            continue;
        if (tileElement->AsTrack()->GetRideIndex() != ride->id)
        {
            // Only check that the track belongs to the same ride if ride does not have buildable track
            if (!ride->GetRideTypeDescriptor().HasFlag(RtdFlag::hasTrack))
                continue;
        }

        if (trackType == TrackElemType::None || trackType == tileElement->AsTrack()->GetTrackType())
        {
            ride_ratings_score_close_proximity(state, tileElement);

            TrackBeginEnd trackBeginEnd;
            if (!TrackBlockGetPrevious({ state.Proximity, tileElement }, &trackBeginEnd))
            {
                state.State = RIDE_RATINGS_STATE_CALCULATE;
                return;
            }

            loc.x = trackBeginEnd.begin_x;
            loc.y = trackBeginEnd.begin_y;
            loc.z = trackBeginEnd.begin_z;
            if (loc == state.ProximityStart)
            {
                state.State = RIDE_RATINGS_STATE_CALCULATE;
                return;
            }
            state.Proximity = loc;
            state.ProximityTrackType = trackBeginEnd.begin_element->AsTrack()->GetTrackType();
            return;
        }
    } while (!(tileElement++)->IsLastForTile());

    state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
}

/**
 *
 *  rct2: 0x006B5BB2
 */
static void ride_ratings_begin_proximity_loop(RideRatingUpdateState& state)
{
    auto ride = GetRide(state.CurrentRide);
    if (ride == nullptr || ride->status == RideStatus::Closed)
    {
        state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
        return;
    }

    const auto& rtd = ride->GetRideTypeDescriptor();
    if (rtd.specialType == RtdSpecialType::maze)
    {
        state.State = RIDE_RATINGS_STATE_CALCULATE;
        return;
    }

    for (auto& station : ride->GetStations())
    {
        if (!station.Start.IsNull())
        {
            state.StationFlags &= ~RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
            if (station.Entrance.IsNull())
            {
                state.StationFlags |= RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
            }

            auto location = station.GetStart();
            state.Proximity = location;
            state.ProximityTrackType = TrackElemType::None;
            state.ProximityStart = location;
            return;
        }
    }

    state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
}

static void proximity_score_increment(RideRatingUpdateState& state, int32_t type)
{
    state.ProximityScores[type]++;
}

/**
 *
 *  rct2: 0x006B6207
 */
static void ride_ratings_score_close_proximity_in_direction(
    RideRatingUpdateState& state, TileElement* inputTileElement, int32_t direction)
{
    auto scorePos = CoordsXY{ CoordsXY{ state.Proximity } + CoordsDirectionDelta[direction] };
    if (!MapIsLocationValid(scorePos))
        return;

    TileElement* tileElement = MapGetFirstElementAt(scorePos);
    if (tileElement == nullptr)
        return;
    do
    {
        if (tileElement->IsGhost())
            continue;

        switch (tileElement->GetType())
        {
            case TileElementType::Surface:
                if (state.ProximityBaseHeight <= inputTileElement->BaseHeight)
                {
                    if (inputTileElement->ClearanceHeight <= tileElement->BaseHeight)
                    {
                        proximity_score_increment(state, PROXIMITY_SURFACE_SIDE_CLOSE);
                    }
                }
                break;
            case TileElementType::Path:
                if (abs(inputTileElement->GetBaseZ() - tileElement->GetBaseZ()) <= 2 * kCoordsZStep)
                {
                    proximity_score_increment(state, PROXIMITY_PATH_SIDE_CLOSE);
                }
                break;
            case TileElementType::Track:
                if (inputTileElement->AsTrack()->GetRideIndex() != tileElement->AsTrack()->GetRideIndex())
                {
                    if (abs(inputTileElement->GetBaseZ() - tileElement->GetBaseZ()) <= 2 * kCoordsZStep)
                    {
                        proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_SIDE_CLOSE);
                    }
                }
                break;
            case TileElementType::SmallScenery:
            case TileElementType::LargeScenery:
                if (tileElement->GetBaseZ() < inputTileElement->GetClearanceZ())
                {
                    if (inputTileElement->GetBaseZ() > tileElement->GetClearanceZ())
                    {
                        proximity_score_increment(state, PROXIMITY_SCENERY_SIDE_ABOVE);
                    }
                    else
                    {
                        proximity_score_increment(state, PROXIMITY_SCENERY_SIDE_BELOW);
                    }
                }
                break;
            default:
                break;
        }
    } while (!(tileElement++)->IsLastForTile());
}

static void ride_ratings_score_close_proximity_loops_helper(RideRatingUpdateState& state, const CoordsXYE& coordsElement)
{
    TileElement* tileElement = MapGetFirstElementAt(coordsElement);
    if (tileElement == nullptr)
        return;
    do
    {
        if (tileElement->IsGhost())
            continue;

        auto type = tileElement->GetType();
        if (type == TileElementType::Path)
        {
            int32_t zDiff = static_cast<int32_t>(tileElement->BaseHeight)
                - static_cast<int32_t>(coordsElement.element->BaseHeight);
            if (zDiff >= 0 && zDiff <= 16)
            {
                proximity_score_increment(state, PROXIMITY_PATH_TROUGH_VERTICAL_LOOP);
            }
        }
        else if (type == TileElementType::Track)
        {
            bool elementsAreAt90DegAngle = ((tileElement->GetDirection() ^ coordsElement.element->GetDirection()) & 1) != 0;
            if (elementsAreAt90DegAngle)
            {
                int32_t zDiff = static_cast<int32_t>(tileElement->BaseHeight)
                    - static_cast<int32_t>(coordsElement.element->BaseHeight);
                if (zDiff >= 0 && zDiff <= 16)
                {
                    proximity_score_increment(state, PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP);
                    if (tileElement->AsTrack()->GetTrackType() == TrackElemType::LeftVerticalLoop
                        || tileElement->AsTrack()->GetTrackType() == TrackElemType::RightVerticalLoop)
                    {
                        proximity_score_increment(state, PROXIMITY_INTERSECTING_VERTICAL_LOOP);
                    }
                }
            }
        }
    } while (!(tileElement++)->IsLastForTile());
}

/**
 *
 *  rct2: 0x006B62DA
 */
static void ride_ratings_score_close_proximity_loops(RideRatingUpdateState& state, TileElement* inputTileElement)
{
    auto trackType = inputTileElement->AsTrack()->GetTrackType();
    if (trackType == TrackElemType::LeftVerticalLoop || trackType == TrackElemType::RightVerticalLoop)
    {
        ride_ratings_score_close_proximity_loops_helper(state, { state.Proximity, inputTileElement });

        int32_t direction = inputTileElement->GetDirection();
        ride_ratings_score_close_proximity_loops_helper(
            state, { CoordsXY{ state.Proximity } + CoordsDirectionDelta[direction], inputTileElement });
    }
}

/**
 *
 *  rct2: 0x006B5F9D
 */
static void ride_ratings_score_close_proximity(RideRatingUpdateState& state, TileElement* inputTileElement)
{
    if (state.StationFlags & RIDE_RATING_STATION_FLAG_NO_ENTRANCE)
    {
        return;
    }

    state.ProximityTotal++;
    TileElement* tileElement = MapGetFirstElementAt(state.Proximity);
    if (tileElement == nullptr)
        return;
    do
    {
        if (tileElement->IsGhost())
            continue;

        int32_t waterHeight;
        switch (tileElement->GetType())
        {
            case TileElementType::Surface:
                state.ProximityBaseHeight = tileElement->BaseHeight;
                if (tileElement->GetBaseZ() == state.Proximity.z)
                {
                    proximity_score_increment(state, PROXIMITY_SURFACE_TOUCH);
                }
                waterHeight = tileElement->AsSurface()->GetWaterHeight();
                if (waterHeight != 0)
                {
                    auto z = waterHeight;
                    if (z <= state.Proximity.z)
                    {
                        proximity_score_increment(state, PROXIMITY_WATER_OVER);
                        if (z == state.Proximity.z)
                        {
                            proximity_score_increment(state, PROXIMITY_WATER_TOUCH);
                        }
                        z += 16;
                        if (z == state.Proximity.z)
                        {
                            proximity_score_increment(state, PROXIMITY_WATER_LOW);
                        }
                        z += 112;
                        if (z <= state.Proximity.z)
                        {
                            proximity_score_increment(state, PROXIMITY_WATER_HIGH);
                        }
                    }
                }
                break;
            case TileElementType::Path:
                if (!tileElement->AsPath()->IsQueue())
                {
                    if (tileElement->GetClearanceZ() == inputTileElement->GetBaseZ())
                    {
                        proximity_score_increment(state, PROXIMITY_PATH_TOUCH_ABOVE);
                    }
                    if (tileElement->GetBaseZ() == inputTileElement->GetClearanceZ())
                    {
                        proximity_score_increment(state, PROXIMITY_PATH_TOUCH_UNDER);
                    }
                }
                else
                {
                    if (tileElement->GetClearanceZ() <= inputTileElement->GetBaseZ())
                    {
                        proximity_score_increment(state, PROXIMITY_QUEUE_PATH_OVER);
                    }
                    if (tileElement->GetClearanceZ() == inputTileElement->GetBaseZ())
                    {
                        proximity_score_increment(state, PROXIMITY_QUEUE_PATH_TOUCH_ABOVE);
                    }
                    if (tileElement->GetBaseZ() == inputTileElement->GetClearanceZ())
                    {
                        proximity_score_increment(state, PROXIMITY_QUEUE_PATH_TOUCH_UNDER);
                    }
                }
                break;
            case TileElementType::Track:
            {
                auto trackType = tileElement->AsTrack()->GetTrackType();
                if (trackType == TrackElemType::LeftVerticalLoop || trackType == TrackElemType::RightVerticalLoop)
                {
                    int32_t sequence = tileElement->AsTrack()->GetSequenceIndex();
                    if (sequence == 3 || sequence == 6)
                    {
                        if (tileElement->BaseHeight - inputTileElement->ClearanceHeight <= 10)
                        {
                            proximity_score_increment(state, PROXIMITY_THROUGH_VERTICAL_LOOP);
                        }
                    }
                }
                if (inputTileElement->AsTrack()->GetRideIndex() != tileElement->AsTrack()->GetRideIndex())
                {
                    proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_ABOVE_OR_BELOW);
                    if (tileElement->GetClearanceZ() == inputTileElement->GetBaseZ())
                    {
                        proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE);
                    }
                    if (tileElement->ClearanceHeight + 2 <= inputTileElement->BaseHeight)
                    {
                        if (tileElement->ClearanceHeight + 10 >= inputTileElement->BaseHeight)
                        {
                            proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE);
                        }
                    }
                    if (inputTileElement->ClearanceHeight == tileElement->BaseHeight)
                    {
                        proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE);
                    }
                    if (inputTileElement->ClearanceHeight + 2 == tileElement->BaseHeight)
                    {
                        if (static_cast<uint8_t>(inputTileElement->ClearanceHeight + 10) >= tileElement->BaseHeight)
                        {
                            proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE);
                        }
                    }
                }
                else
                {
                    bool isStation = tileElement->AsTrack()->IsStation();
                    if (tileElement->ClearanceHeight == inputTileElement->BaseHeight)
                    {
                        proximity_score_increment(state, PROXIMITY_OWN_TRACK_TOUCH_ABOVE);
                        if (isStation)
                        {
                            proximity_score_increment(state, PROXIMITY_OWN_STATION_TOUCH_ABOVE);
                        }
                    }
                    if (tileElement->ClearanceHeight + 2 <= inputTileElement->BaseHeight)
                    {
                        if (tileElement->ClearanceHeight + 10 >= inputTileElement->BaseHeight)
                        {
                            proximity_score_increment(state, PROXIMITY_OWN_TRACK_CLOSE_ABOVE);
                            if (isStation)
                            {
                                proximity_score_increment(state, PROXIMITY_OWN_STATION_CLOSE_ABOVE);
                            }
                        }
                    }

                    if (inputTileElement->GetClearanceZ() == tileElement->GetBaseZ())
                    {
                        proximity_score_increment(state, PROXIMITY_OWN_TRACK_TOUCH_ABOVE);
                        if (isStation)
                        {
                            proximity_score_increment(state, PROXIMITY_OWN_STATION_TOUCH_ABOVE);
                        }
                    }
                    if (inputTileElement->ClearanceHeight + 2 <= tileElement->BaseHeight)
                    {
                        if (inputTileElement->ClearanceHeight + 10 >= tileElement->BaseHeight)
                        {
                            proximity_score_increment(state, PROXIMITY_OWN_TRACK_CLOSE_ABOVE);
                            if (isStation)
                            {
                                proximity_score_increment(state, PROXIMITY_OWN_STATION_CLOSE_ABOVE);
                            }
                        }
                    }
                }
                break;
            }
            default:
                break;
        } // switch tileElement->GetType
    } while (!(tileElement++)->IsLastForTile());

    uint8_t direction = inputTileElement->GetDirection();
    ride_ratings_score_close_proximity_in_direction(state, inputTileElement, (direction + 1) & 3);
    ride_ratings_score_close_proximity_in_direction(state, inputTileElement, (direction - 1) & 3);
    ride_ratings_score_close_proximity_loops(state, inputTileElement);

    switch (state.ProximityTrackType)
    {
        case TrackElemType::Brakes:
            state.AmountOfBrakes++;
            break;
        case TrackElemType::LeftReverser:
        case TrackElemType::RightReverser:
            state.AmountOfReversers++;
            break;
        default:
            break;
    }
}

static void RideRatingsCalculate(RideRatingUpdateState& state, Ride& ride)
{
    const auto& rrd = ride.GetRideTypeDescriptor().RatingsData;

    switch (rrd.Type)
    {
        case RatingsCalculationType::Normal:
            if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_TESTED))
                return;
            break;
        case RatingsCalculationType::FlatRide:
            ride.lifecycle_flags |= RIDE_LIFECYCLE_TESTED;
            ride.lifecycle_flags |= RIDE_LIFECYCLE_NO_RAW_STATS;
            break;
        case RatingsCalculationType::Stall:
            ride.upkeep_cost = RideComputeUpkeep(state, ride);
            ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME;
            // Exit ratings
            return;
    }

    ride.unreliability_factor = rrd.Unreliability;
    SetUnreliabilityFactor(ride);

    const auto shelteredEighths = GetNumOfShelteredEighths(ride);
    ride.sheltered_eighths = (rrd.RideShelter == -1) ? shelteredEighths.TotalShelteredEighths : rrd.RideShelter;

    RatingTuple ratings = rrd.BaseRatings;
    // Apply Modifiers
    for (const auto& modifier : rrd.Modifiers)
    {
        switch (modifier.type)
        {
            case RatingsModifierType::BonusLength:
                RideRatingsApplyBonusLength(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusSynchronisation:
                RideRatingsApplyBonusSynchronisation(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusTrainLength:
                RideRatingsApplyBonusTrainLength(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusMaxSpeed:
                RideRatingsApplyBonusMaxSpeed(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusAverageSpeed:
                RideRatingsApplyBonusAverageSpeed(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusDuration:
                RideRatingsApplyBonusDuration(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusGForces:
                RideRatingsApplyBonusGForces(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusTurns:
                RideRatingsApplyBonusTurns(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusDrops:
                RideRatingsApplyBonusDrops(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusSheltered:
                RideRatingsApplyBonusSheltered(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusProximity:
                RideRatingsApplyBonusProximity(ratings, ride, state, modifier);
                break;
            case RatingsModifierType::BonusScenery:
                RideRatingsApplyBonusScenery(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusRotations:
                RideRatingsApplyBonusRotations(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusOperationOption:
                RideRatingsApplyBonusOperationOption(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusReversedTrains:
                RideRatingsApplyBonusReversedTrains(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusGoKartRace:
                RideRatingsApplyBonusGoKartRace(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusTowerRide:
                RideRatingsApplyBonusTowerRide(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusRotoDrop:
                RideRatingsApplyBonusRotoDrop(ratings, ride);
                break;
            case RatingsModifierType::BonusMazeSize:
                RideRatingsApplyBonusMazeSize(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusBoatHireNoCircuit:
                RideRatingsApplyBonusBoatHireNoCircuit(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusSlideUnlimitedRides:
                RideRatingsApplyBonusSlideUnlimitedRides(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusMotionSimulatorMode:
                RideRatingsApplyBonusMotionSimulatorMode(ratings, ride, modifier);
                break;
            case RatingsModifierType::Bonus3DCinemaMode:
                RideRatingsApplyBonus3DCinemaMode(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusTopSpinMode:
                RideRatingsApplyBonusTopSpinMode(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusReversals:
                RideRatingsApplyBonusReversals(ratings, ride, state, modifier);
                break;
            case RatingsModifierType::BonusHoles:
                RideRatingsApplyBonusHoles(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusNumTrains:
                RideRatingsApplyBonusNumTrains(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusDownwardLaunch:
                RideRatingsApplyBonusDownwardLaunch(ratings, ride, modifier);
                break;
            case RatingsModifierType::BonusLaunchedFreefallSpecial:
                RideRatingsApplyBonusLaunchedFreefallSpecial(ratings, ride, state, modifier);
                break;
            case RatingsModifierType::RequirementLength:
                RideRatingsApplyRequirementLength(ratings, ride, modifier);
                break;
            case RatingsModifierType::RequirementMaxSpeed:
                RideRatingsApplyRequirementMaxSpeed(ratings, ride, modifier);
                break;
            case RatingsModifierType::RequirementLateralGs:
                RideRatingsApplyRequirementLateralGs(ratings, ride, modifier);
                break;
            case RatingsModifierType::RequirementInversions:
                RideRatingsApplyRequirementInversions(ratings, ride, modifier);
                break;
            case RatingsModifierType::RequirementUnsheltered:
                RideRatingsApplyRequirementUnsheltered(ratings, ride, shelteredEighths.TrackShelteredEighths, modifier);
                break;
            case RatingsModifierType::RequirementReversals:
                RideRatingsApplyRequirementReversals(ratings, ride, state, modifier);
                break;
            case RatingsModifierType::RequirementHoles:
                RideRatingsApplyRequirementHoles(ratings, ride, modifier);
                break;
            case RatingsModifierType::RequirementStations:
                RideRatingsApplyRequirementStations(ratings, ride, modifier);
                break;
            case RatingsModifierType::RequirementSplashdown:
                RideRatingsApplyRequirementSplashdown(ratings, ride, modifier);
                break;
            case RatingsModifierType::PenaltyLateralGs:
                RideRatingsApplyPenaltyLateralGs(ratings, ride, modifier);
                break;
            default:
                break;
        }

        // Requirements that may be ignored if the ride has inversions
        if (ride.inversions == 0 || !rrd.RelaxRequirementsIfInversions)
        {
            switch (modifier.type)
            {
                case RatingsModifierType::RequirementDropHeight:
                    RideRatingsApplyRequirementDropHeight(ratings, ride, modifier);
                    break;
                case RatingsModifierType::RequirementNumDrops:
                    RideRatingsApplyRequirementNumDrops(ratings, ride, modifier);
                    break;
                case RatingsModifierType::RequirementNegativeGs:
                    RideRatingsApplyRequirementNegativeGs(ratings, ride, modifier);
                    break;
                default:
                    break;
            }
        }
    }
    // Universl ratings adjustments
    RideRatingsApplyIntensityPenalty(ratings);
    RideRatingsApplyAdjustments(ride, ratings);
    ride.ratings = ratings;

    ride.upkeep_cost = RideComputeUpkeep(state, ride);
    ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME;

#ifdef ORIGINAL_RATINGS
    if (!ride.ratings.isNull())
    {
        // Address underflows allowed by original RCT2 code
        ride.ratings.excitement = std::max<uint16_t>(0, ride.ratings.excitement);
        ride.ratings.intensity = std::max<uint16_t>(0, ride.ratings.intensity);
        ride.ratings.nausea = std::max<uint16_t>(0, ride.ratings.nausea);
    }
#endif

#ifdef ENABLE_SCRIPTING
    // Only call the 'ride.ratings.calculate' API hook if testing of the ride is complete
    if (ride.lifecycle_flags & RIDE_LIFECYCLE_TESTED)
    {
        auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine();
        if (hookEngine.HasSubscriptions(HOOK_TYPE::RIDE_RATINGS_CALCULATE))
        {
            auto ctx = GetContext()->GetScriptEngine().GetContext();
            auto originalRatings = ride.ratings;

            // Create event args object
            auto obj = DukObject(ctx);
            obj.Set("rideId", ride.id.ToUnderlying());
            obj.Set("excitement", originalRatings.excitement);
            obj.Set("intensity", originalRatings.intensity);
            obj.Set("nausea", originalRatings.nausea);

            // Call the subscriptions
            auto e = obj.Take();
            hookEngine.Call(HOOK_TYPE::RIDE_RATINGS_CALCULATE, e, true);

            auto scriptExcitement = AsOrDefault(e["excitement"], static_cast<int32_t>(originalRatings.excitement));
            auto scriptIntensity = AsOrDefault(e["intensity"], static_cast<int32_t>(originalRatings.intensity));
            auto scriptNausea = AsOrDefault(e["nausea"], static_cast<int32_t>(originalRatings.nausea));

            ride.ratings.excitement = std::clamp<int32_t>(scriptExcitement, 0, INT16_MAX);
            ride.ratings.intensity = std::clamp<int32_t>(scriptIntensity, 0, INT16_MAX);
            ride.ratings.nausea = std::clamp<int32_t>(scriptNausea, 0, INT16_MAX);
        }
    }
#endif
}

static void RideRatingsCalculateValue(Ride& ride)
{
    struct Row
    {
        int32_t months, multiplier, divisor, summand;
    };
    static const Row ageTableNew[] = {
        { 5, 3, 2, 0 },       // 1.5x
        { 13, 6, 5, 0 },      // 1.2x
        { 40, 1, 1, 0 },      // 1x
        { 64, 3, 4, 0 },      // 0.75x
        { 88, 9, 16, 0 },     // 0.56x
        { 104, 27, 64, 0 },   // 0.42x
        { 120, 81, 256, 0 },  // 0.32x
        { 128, 81, 512, 0 },  // 0.16x
        { 200, 81, 1024, 0 }, // 0.08x
        { 200, 9, 16, 0 },    // 0.56x "easter egg"
    };

#ifdef ORIGINAL_RATINGS
    static const Row ageTableOld[] = {
        { 5, 1, 1, 30 },      // +30
        { 13, 1, 1, 10 },     // +10
        { 40, 1, 1, 0 },      // 1x
        { 64, 3, 4, 0 },      // 0.75x
        { 88, 9, 16, 0 },     // 0.56x
        { 104, 27, 64, 0 },   // 0.42x
        { 120, 81, 256, 0 },  // 0.32x
        { 128, 81, 512, 0 },  // 0.16x
        { 200, 81, 1024, 0 }, // 0.08x
        { 200, 9, 16, 0 },    // 0.56x "easter egg"
    };
#endif

    if (!RideHasRatings(ride))
    {
        return;
    }

    // Start with the base ratings, multiplied by the ride type specific weights for excitement, intensity and nausea.
    const auto& ratingsMultipliers = ride.GetRideTypeDescriptor().RatingsMultipliers;
    money64 value = (((ride.ratings.excitement * ratingsMultipliers.excitement) * 32) >> 15)
        + (((ride.ratings.intensity * ratingsMultipliers.intensity) * 32) >> 15)
        + (((ride.ratings.nausea * ratingsMultipliers.nausea) * 32) >> 15);

    int32_t monthsOld = 0;
    if (!GetGameState().Cheats.disableRideValueAging)
    {
        monthsOld = ride.GetAge();
    }

    const Row* ageTable = ageTableNew;
    size_t tableSize = std::size(ageTableNew);

#ifdef ORIGINAL_RATINGS
    ageTable = ageTableOld;
    tableSize = std::size(ageTableOld);
#endif

    Row lastRow = ageTable[tableSize - 1];

    // Ride is older than oldest age in the table?
    if (monthsOld >= lastRow.months)
    {
        value = (value * lastRow.multiplier) / lastRow.divisor + lastRow.summand;
    }
    else
    {
        // Find the first hit in the table that matches this ride's age
        for (size_t it = 0; it < tableSize; it++)
        {
            Row curr = ageTable[it];

            if (monthsOld < curr.months)
            {
                value = (value * curr.multiplier) / curr.divisor + curr.summand;
                break;
            }
        }
    }

    // Other ride of same type penalty
    const auto& rideManager = GetRideManager();
    auto rideType = ride.type;
    auto otherRidesOfSameType = std::count_if(rideManager.begin(), rideManager.end(), [rideType](const Ride& r) {
        return r.status == RideStatus::Open && r.type == rideType;
    });
    if (otherRidesOfSameType > 1)
        value -= value / 4;

    ride.value = std::max(0.00_GBP, value);
}

/**
 * I think this function computes ride upkeep? Though it is weird that the
 *  rct2: Sub65E621
 * inputs
 * - edi: ride ptr
 */
static money64 RideComputeUpkeep(RideRatingUpdateState& state, const Ride& ride)
{
    // data stored at 0x0057E3A8, incrementing 18 bytes at a time
    auto upkeep = ride.GetRideTypeDescriptor().UpkeepCosts.BaseCost;

    auto trackCost = ride.GetRideTypeDescriptor().UpkeepCosts.CostPerTrackPiece;
    upkeep += trackCost * ride.getNumPoweredLifts();

    uint32_t totalLength = ToHumanReadableRideLength(ride.GetTotalLength());

    // The data originally here was 20's and 0's. The 20's all represented
    // rides that had tracks. The 0's were fixed rides like crooked house or
    // dodgems.
    // Data source is 0x0097E3AC
    totalLength *= ride.GetRideTypeDescriptor().UpkeepCosts.TrackLengthMultiplier;
    upkeep += static_cast<uint16_t>(totalLength >> 10);

    if (ride.lifecycle_flags & RIDE_LIFECYCLE_ON_RIDE_PHOTO)
    {
        // The original code read from a table starting at 0x0097E3AE and
        // incrementing by 0x12 bytes between values. However, all of these
        // values were 40. I have replaced the table lookup with the constant
        // 40 in this case.
        upkeep += 40;
    }

    // Add maintenance cost for reverser track pieces
    upkeep += 10 * state.AmountOfReversers;

    // Add maintenance cost for brake track pieces
    upkeep += 20 * state.AmountOfBrakes;

    // these seem to be adhoc adjustments to a ride's upkeep/cost, times
    // various variables set on the ride itself.

    // https://gist.github.com/kevinburke/e19b803cd2769d96c540
    upkeep += ride.GetRideTypeDescriptor().UpkeepCosts.CostPerTrain * ride.NumTrains;
    upkeep += ride.GetRideTypeDescriptor().UpkeepCosts.CostPerCar * ride.num_cars_per_train;

    // slight upkeep boosts for some rides - 5 for mini railway, 10 for log
    // flume/rapids, 10 for roller coaster, 28 for giga coaster
    upkeep += ride.GetRideTypeDescriptor().UpkeepCosts.CostPerStation * ride.num_stations;

    if (ride.mode == RideMode::ReverseInclineLaunchedShuttle)
    {
        upkeep += 30;
    }
    else if (ride.mode == RideMode::PoweredLaunchPasstrough)
    {
        upkeep += 160;
    }
    else if (ride.mode == RideMode::LimPoweredLaunch)
    {
        upkeep += 320;
    }
    else if (ride.mode == RideMode::PoweredLaunch || ride.mode == RideMode::PoweredLaunchBlockSectioned)
    {
        upkeep += 220;
    }

    // multiply by 5/8
    upkeep *= 10;
    upkeep >>= 4;
    return upkeep;
}

/**
 *
 *  rct2: 0x0065E7FB
 *
 * inputs
 * - bx: excitement
 * - cx: intensity
 * - bp: nausea
 * - edi: ride ptr
 */
static void RideRatingsApplyAdjustments(const Ride& ride, RatingTuple& ratings)
{
    const auto* rideEntry = GetRideEntryByIndex(ride.subtype);

    if (rideEntry == nullptr)
    {
        return;
    }

    // Apply ride entry multipliers
    RideRatingsAdd(
        ratings, ((static_cast<int32_t>(ratings.excitement) * rideEntry->excitement_multiplier) >> 7),
        ((static_cast<int32_t>(ratings.intensity) * rideEntry->intensity_multiplier) >> 7),
        ((static_cast<int32_t>(ratings.nausea) * rideEntry->nausea_multiplier) >> 7));

    // Apply total air time
#ifdef ORIGINAL_RATINGS
    if (ride.GetRideTypeDescriptor().HasFlag(RtdFlag::hasAirTime))
    {
        uint16_t totalAirTime = ride.totalAirTime;
        if (rideEntry->flags & RIDE_ENTRY_FLAG_LIMIT_AIRTIME_BONUS)
        {
            if (totalAirTime >= 96)
            {
                totalAirTime -= 96;
                ratings.excitement -= totalAirTime / 8;
                ratings.nausea += totalAirTime / 16;
            }
        }
        else
        {
            ratings.excitement += totalAirTime / 8;
            ratings.nausea += totalAirTime / 16;
        }
    }
#else
    if (ride.GetRideTypeDescriptor().HasFlag(RtdFlag::hasAirTime))
    {
        int32_t excitementModifier;
        if (rideEntry->flags & RIDE_ENTRY_FLAG_LIMIT_AIRTIME_BONUS)
        {
            // Limit airtime bonus for heartline twister coaster (see issues #2031 and #2064)
            excitementModifier = std::min<uint16_t>(ride.totalAirTime, 96) / 8;
        }
        else
        {
            excitementModifier = ride.totalAirTime / 8;
        }
        int32_t nauseaModifier = ride.totalAirTime / 16;

        RideRatingsAdd(ratings, excitementModifier, 0, nauseaModifier);
    }
#endif
}

/**
 * Lowers excitement, the higher the intensity.
 *  rct2: 0x0065E7A3
 */
static void RideRatingsApplyIntensityPenalty(RatingTuple& ratings)
{
    static constexpr ride_rating intensityBounds[] = { 1000, 1100, 1200, 1320, 1450 };
    ride_rating excitement = ratings.excitement;
    for (auto intensityBound : intensityBounds)
    {
        if (ratings.intensity >= intensityBound)
        {
            excitement -= excitement / 4;
        }
    }
    ratings.excitement = excitement;
}

/**
 *
 *  rct2: 0x00655FD6
 */
static void SetUnreliabilityFactor(Ride& ride)
{
    // Special unreliability for a few ride types
    if (ride.type == RIDE_TYPE_COMPACT_INVERTED_COASTER && ride.mode == RideMode::ReverseInclineLaunchedShuttle)
    {
        ride.unreliability_factor += 10;
    }
    else if (ride.type == RIDE_TYPE_LOOPING_ROLLER_COASTER && ride.IsPoweredLaunched())
    {
        ride.unreliability_factor += 5;
    }
    else if (ride.type == RIDE_TYPE_CHAIRLIFT)
    {
        ride.unreliability_factor += (ride.speed * 2);
    }
    // The bigger the difference in lift speed and minimum the higher the unreliability
    uint8_t minLiftSpeed = ride.GetRideTypeDescriptor().LiftData.minimum_speed;
    ride.unreliability_factor += (ride.lift_hill_speed - minLiftSpeed) * 2;
}

static uint32_t get_proximity_score_helper_1(uint16_t x, uint16_t max, uint32_t multiplier)
{
    return (std::min(x, max) * multiplier) >> 16;
}

static uint32_t get_proximity_score_helper_2(uint16_t x, uint16_t additionIfNotZero, uint16_t max, uint32_t multiplier)
{
    uint32_t result = x;
    if (result != 0)
        result += additionIfNotZero;
    return (std::min<int32_t>(result, max) * multiplier) >> 16;
}

static uint32_t get_proximity_score_helper_3(uint16_t x, uint16_t resultIfNotZero)
{
    return x == 0 ? 0 : resultIfNotZero;
}

/**
 *
 *  rct2: 0x0065E277
 */
static uint32_t ride_ratings_get_proximity_score(RideRatingUpdateState& state)
{
    const uint16_t* scores = state.ProximityScores;

    uint32_t result = 0;
    result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_OVER], 60, 0x00AAAA);
    result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_TOUCH], 22, 0x0245D1);
    result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_LOW], 10, 0x020000);
    result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_HIGH], 40, 0x00A000);
    result += get_proximity_score_helper_1(scores[PROXIMITY_SURFACE_TOUCH], 70, 0x01B6DB);
    result += get_proximity_score_helper_1(scores[PROXIMITY_QUEUE_PATH_OVER] + 8, 12, 0x064000);
    result += get_proximity_score_helper_3(scores[PROXIMITY_QUEUE_PATH_TOUCH_ABOVE], 40);
    result += get_proximity_score_helper_3(scores[PROXIMITY_QUEUE_PATH_TOUCH_UNDER], 45);
    result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_TOUCH_ABOVE], 10, 20, 0x03C000);
    result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_TOUCH_UNDER], 10, 20, 0x044000);
    result += get_proximity_score_helper_2(scores[PROXIMITY_OWN_TRACK_TOUCH_ABOVE], 10, 15, 0x035555);
    result += get_proximity_score_helper_1(scores[PROXIMITY_OWN_TRACK_CLOSE_ABOVE], 5, 0x060000);
    result += get_proximity_score_helper_2(scores[PROXIMITY_FOREIGN_TRACK_ABOVE_OR_BELOW], 10, 15, 0x02AAAA);
    result += get_proximity_score_helper_2(scores[PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE], 10, 15, 0x04AAAA);
    result += get_proximity_score_helper_1(scores[PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE], 5, 0x090000);
    result += get_proximity_score_helper_1(scores[PROXIMITY_SCENERY_SIDE_BELOW], 35, 0x016DB6);
    result += get_proximity_score_helper_1(scores[PROXIMITY_SCENERY_SIDE_ABOVE], 35, 0x00DB6D);
    result += get_proximity_score_helper_3(scores[PROXIMITY_OWN_STATION_TOUCH_ABOVE], 55);
    result += get_proximity_score_helper_3(scores[PROXIMITY_OWN_STATION_CLOSE_ABOVE], 25);
    result += get_proximity_score_helper_2(scores[PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP], 4, 6, 0x140000);
    result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_TROUGH_VERTICAL_LOOP], 4, 6, 0x0F0000);
    result += get_proximity_score_helper_3(scores[PROXIMITY_INTERSECTING_VERTICAL_LOOP], 100);
    result += get_proximity_score_helper_2(scores[PROXIMITY_THROUGH_VERTICAL_LOOP], 4, 6, 0x0A0000);
    result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_SIDE_CLOSE], 10, 20, 0x01C000);
    result += get_proximity_score_helper_2(scores[PROXIMITY_FOREIGN_TRACK_SIDE_CLOSE], 10, 20, 0x024000);
    result += get_proximity_score_helper_2(scores[PROXIMITY_SURFACE_SIDE_CLOSE], 10, 20, 0x028000);
    return result;
}

/**
 * Calculates how much of the track is sheltered in eighths.
 *  rct2: 0x0065E72D
 */
static ShelteredEights GetNumOfShelteredEighths(const Ride& ride)
{
    int32_t totalLength = ride.GetTotalLength();
    int32_t shelteredLength = ride.sheltered_length;
    int32_t lengthEighth = totalLength / 8;
    int32_t lengthCounter = lengthEighth;
    uint8_t numShelteredEighths = 0;
    for (int32_t i = 0; i < 7; i++)
    {
        if (shelteredLength >= lengthCounter)
        {
            lengthCounter += lengthEighth;
            numShelteredEighths++;
        }
    }

    uint8_t trackShelteredEighths = numShelteredEighths;
    const auto* rideType = GetRideEntryByIndex(ride.subtype);
    if (rideType == nullptr)
    {
        return { 0, 0 };
    }
    if (rideType->flags & RIDE_ENTRY_FLAG_COVERED_RIDE)
        numShelteredEighths = 7;

    return { trackShelteredEighths, numShelteredEighths };
}

static RatingTuple get_flat_turns_rating(const Ride& ride)
{
    int32_t num3PlusTurns = GetTurnCount3Elements(ride, 0);
    int32_t num2Turns = GetTurnCount2Elements(ride, 0);
    int32_t num1Turns = GetTurnCount1Element(ride, 0);

    RatingTuple rating;
    rating.excitement = (num3PlusTurns * 0x28000) >> 16;
    rating.excitement += (num2Turns * 0x30000) >> 16;
    rating.excitement += (num1Turns * 63421) >> 16;

    rating.intensity = (num3PlusTurns * 81920) >> 16;
    rating.intensity += (num2Turns * 49152) >> 16;
    rating.intensity += (num1Turns * 21140) >> 16;

    rating.nausea = (num3PlusTurns * 0x50000) >> 16;
    rating.nausea += (num2Turns * 0x32000) >> 16;
    rating.nausea += (num1Turns * 42281) >> 16;

    return rating;
}

/**
 *
 *  rct2: 0x0065DF72
 */
static RatingTuple get_banked_turns_rating(const Ride& ride)
{
    int32_t num3PlusTurns = GetTurnCount3Elements(ride, 1);
    int32_t num2Turns = GetTurnCount2Elements(ride, 1);
    int32_t num1Turns = GetTurnCount1Element(ride, 1);

    RatingTuple rating;
    rating.excitement = (num3PlusTurns * 0x3C000) >> 16;
    rating.excitement += (num2Turns * 0x3C000) >> 16;
    rating.excitement += (num1Turns * 73992) >> 16;

    rating.intensity = (num3PlusTurns * 0x14000) >> 16;
    rating.intensity += (num2Turns * 49152) >> 16;
    rating.intensity += (num1Turns * 21140) >> 16;

    rating.nausea = (num3PlusTurns * 0x50000) >> 16;
    rating.nausea += (num2Turns * 0x32000) >> 16;
    rating.nausea += (num1Turns * 48623) >> 16;

    return rating;
}

/**
 *
 *  rct2: 0x0065E047
 */
static RatingTuple get_sloped_turns_rating(const Ride& ride)
{
    RatingTuple rating;

    int32_t num4PlusTurns = GetTurnCount4PlusElements(ride, 2);
    int32_t num3Turns = GetTurnCount3Elements(ride, 2);
    int32_t num2Turns = GetTurnCount2Elements(ride, 2);
    int32_t num1Turns = GetTurnCount1Element(ride, 2);

    rating.excitement = (std::min(num4PlusTurns, 4) * 0x78000) >> 16;
    rating.excitement += (std::min(num3Turns, 6) * 273066) >> 16;
    rating.excitement += (std::min(num2Turns, 6) * 0x3AAAA) >> 16;
    rating.excitement += (std::min(num1Turns, 7) * 187245) >> 16;
    rating.intensity = 0;
    rating.nausea = (std::min(num4PlusTurns, 8) * 0x78000) >> 16;

    return rating;
}

/**
 *
 *  rct2: 0x0065E0F2
 */
static RatingTuple get_inversions_ratings(uint16_t inversions)
{
    RatingTuple rating;

    rating.excitement = (std::min<int32_t>(inversions, 6) * 0x1AAAAA) >> 16;
    rating.intensity = (inversions * 0x320000) >> 16;
    rating.nausea = (inversions * 0x15AAAA) >> 16;

    return rating;
}

void SpecialTrackElementRatingsAjustment_Default(const Ride& ride, int32_t& excitement, int32_t& intensity, int32_t& nausea)
{
    if (ride.HasWaterSplash())
    {
        excitement += 50;
        intensity += 30;
        nausea += 20;
    }
    if (ride.HasWaterfall())
    {
        excitement += 55;
        intensity += 30;
    }
    if (ride.HasWhirlpool())
    {
        excitement += 35;
        intensity += 20;
        nausea += 23;
    }
}

void SpecialTrackElementRatingsAjustment_GhostTrain(const Ride& ride, int32_t& excitement, int32_t& intensity, int32_t& nausea)
{
    if (ride.HasSpinningTunnel())
    {
        excitement += 40;
        intensity += 25;
        nausea += 55;
    }
}

void SpecialTrackElementRatingsAjustment_LogFlume(const Ride& ride, int32_t& excitement, int32_t& intensity, int32_t& nausea)
{
    if (ride.HasLogReverser())
    {
        excitement += 48;
        intensity += 55;
        nausea += 65;
    }
}

static RatingTuple GetSpecialTrackElementsRating(uint8_t type, const Ride& ride)
{
    int32_t excitement = 0, intensity = 0, nausea = 0;
    const auto& rtd = ride.GetRideTypeDescriptor();
    rtd.SpecialElementRatingAdjustment(ride, excitement, intensity, nausea);

    uint8_t helixSections = RideGetHelixSections(ride);

    int32_t helixesUpTo9 = std::min<int32_t>(helixSections, 9);
    excitement += (helixesUpTo9 * 254862) >> 16;

    int32_t helixesUpTo11 = std::min<int32_t>(helixSections, 11);
    intensity += (helixesUpTo11 * 148945) >> 16;

    int32_t helixesOver5UpTo10 = std::clamp<int32_t>(helixSections - 5, 0, 10);
    nausea += (helixesOver5UpTo10 * 0x140000) >> 16;

    RatingTuple rating = { static_cast<ride_rating>(excitement), static_cast<ride_rating>(intensity),
                           static_cast<ride_rating>(nausea) };
    return rating;
}

/**
 *
 *  rct2: 0x0065DDD1
 */
static RatingTuple ride_ratings_get_turns_ratings(const Ride& ride)
{
    int32_t excitement = 0, intensity = 0, nausea = 0;

    RatingTuple specialTrackElementsRating = GetSpecialTrackElementsRating(ride.type, ride);
    excitement += specialTrackElementsRating.excitement;
    intensity += specialTrackElementsRating.intensity;
    nausea += specialTrackElementsRating.nausea;

    RatingTuple flatTurnsRating = get_flat_turns_rating(ride);
    excitement += flatTurnsRating.excitement;
    intensity += flatTurnsRating.intensity;
    nausea += flatTurnsRating.nausea;

    RatingTuple bankedTurnsRating = get_banked_turns_rating(ride);
    excitement += bankedTurnsRating.excitement;
    intensity += bankedTurnsRating.intensity;
    nausea += bankedTurnsRating.nausea;

    RatingTuple slopedTurnsRating = get_sloped_turns_rating(ride);
    excitement += slopedTurnsRating.excitement;
    intensity += slopedTurnsRating.intensity;
    nausea += slopedTurnsRating.nausea;

    auto inversions = ride.GetRideTypeDescriptor().specialType == RtdSpecialType::miniGolf ? ride.holes : ride.inversions;
    RatingTuple inversionsRating = get_inversions_ratings(inversions);
    excitement += inversionsRating.excitement;
    intensity += inversionsRating.intensity;
    nausea += inversionsRating.nausea;

    RatingTuple rating = { static_cast<ride_rating>(excitement), static_cast<ride_rating>(intensity),
                           static_cast<ride_rating>(nausea) };
    return rating;
}

/**
 *
 *  rct2: 0x0065E1C2
 */
static RatingTuple ride_ratings_get_sheltered_ratings(const Ride& ride)
{
    int32_t shelteredLengthShifted = (ride.sheltered_length) >> 16;

    uint32_t shelteredLengthUpTo1000 = std::min(shelteredLengthShifted, 1000);
    uint32_t shelteredLengthUpTo2000 = std::min(shelteredLengthShifted, 2000);

    int32_t excitement = (shelteredLengthUpTo1000 * 9175) >> 16;
    int32_t intensity = (shelteredLengthUpTo2000 * 0x2666) >> 16;
    int32_t nausea = (shelteredLengthUpTo1000 * 0x4000) >> 16;

    /*eax = (ride.var_11C * 30340) >> 16;*/
    /*nausea += eax;*/

    if (ride.num_sheltered_sections & ShelteredSectionsBits::kBankingWhileSheltered)
    {
        excitement += 20;
        nausea += 15;
    }

    if (ride.num_sheltered_sections & ShelteredSectionsBits::kRotatingWhileSheltered)
    {
        excitement += 20;
        nausea += 15;
    }

    uint8_t lowerVal = ride.GetNumShelteredSections();
    lowerVal = std::min<uint8_t>(lowerVal, 11);
    excitement += (lowerVal * 774516) >> 16;

    RatingTuple rating = { static_cast<ride_rating>(excitement), static_cast<ride_rating>(intensity),
                           static_cast<ride_rating>(nausea) };
    return rating;
}

/**
 *
 *  rct2: 0x0065DCDC
 */
static RatingTuple ride_ratings_get_gforce_ratings(const Ride& ride)
{
    RatingTuple result = {
        .excitement = 0,
        .intensity = 0,
        .nausea = 0,
    };

    // Apply maximum positive G force factor
    result.excitement += (ride.max_positive_vertical_g * 5242) >> 16;
    result.intensity += (ride.max_positive_vertical_g * 52428) >> 16;
    result.nausea += (ride.max_positive_vertical_g * 17039) >> 16;

    // Apply maximum negative G force factor
    fixed16_2dp gforce = ride.max_negative_vertical_g;
    result.excitement += (std::clamp<fixed16_2dp>(gforce, -MakeRideRating(2, 50), MakeRideRating(0, 00)) * -15728) >> 16;
    result.intensity += ((gforce - MakeRideRating(1, 00)) * -52428) >> 16;
    result.nausea += ((gforce - MakeRideRating(1, 00)) * -14563) >> 16;

    // Apply lateral G force factor
    result.excitement += (std::min<fixed16_2dp>(MakeRideRating(1, 50), ride.max_lateral_g) * 26214) >> 16;
    result.intensity += ride.max_lateral_g;
    result.nausea += (ride.max_lateral_g * 21845) >> 16;

// Very high lateral G force penalty
#ifdef ORIGINAL_RATINGS
    if (ride.max_lateral_g > MakeFixed16_2dp(2, 80))
    {
        result.intensity += MakeRideRating(3, 75);
        result.nausea += MakeRideRating(2, 00);
    }
    if (ride.max_lateral_g > MakeFixed16_2dp(3, 10))
    {
        result.excitement /= 2;
        result.intensity += MakeRideRating(8, 50);
        result.nausea += MakeRideRating(4, 00);
    }
#endif

    return result;
}

/**
 *
 *  rct2: 0x0065E139
 */
static RatingTuple ride_ratings_get_drop_ratings(const Ride& ride)
{
    RatingTuple result = {
        /* .excitement = */ 0,
        /* .intensity = */ 0,
        /* .nausea = */ 0,
    };

    // Apply number of drops factor
    int32_t drops = ride.getNumDrops();
    result.excitement += (std::min(9, drops) * 728177) >> 16;
    result.intensity += (drops * 928426) >> 16;
    result.nausea += (drops * 655360) >> 16;

    // Apply highest drop factor
    RideRatingsAdd(
        result, ((ride.highest_drop_height * 2) * 16000) >> 16, ((ride.highest_drop_height * 2) * 32000) >> 16,
        ((ride.highest_drop_height * 2) * 10240) >> 16);

    return result;
}

/**
 * Calculates a score based on the surrounding scenery.
 *  rct2: 0x0065E557
 */
static int32_t ride_ratings_get_scenery_score(const Ride& ride)
{
    auto stationIndex = RideGetFirstValidStationStart(ride);
    CoordsXY location;

    if (stationIndex.IsNull())
    {
        return 0;
    }

    const auto& rtd = ride.GetRideTypeDescriptor();
    if (rtd.specialType == RtdSpecialType::maze)
    {
        location = ride.GetStation().Entrance.ToCoordsXY();
    }
    else
    {
        location = ride.GetStation(stationIndex).Start;
    }

    int32_t z = TileElementHeight(location);

    // Check if station is underground, returns a fixed mediocre score since you can't have scenery underground
    if (z > ride.GetStation(stationIndex).GetBaseZ())
    {
        return 40;
    }

    // Count surrounding scenery items
    int32_t numSceneryItems = 0;
    auto tileLocation = TileCoordsXY(location);
    auto& gameState = GetGameState();
    for (int32_t yy = std::max(tileLocation.y - 5, 0); yy <= std::min(tileLocation.y + 5, gameState.MapSize.y - 1); yy++)
    {
        for (int32_t xx = std::max(tileLocation.x - 5, 0); xx <= std::min(tileLocation.x + 5, gameState.MapSize.x - 1); xx++)
        {
            // Count scenery items on this tile
            TileElement* tileElement = MapGetFirstElementAt(TileCoordsXY{ xx, yy });
            if (tileElement == nullptr)
                continue;
            do
            {
                if (tileElement->IsGhost())
                    continue;

                const auto type = tileElement->GetType();
                if (type == TileElementType::SmallScenery || type == TileElementType::LargeScenery)
                    numSceneryItems++;
            } while (!(tileElement++)->IsLastForTile());
        }
    }

    return std::min(numSceneryItems, 47) * 5;
}

#pragma region Ride rating calculation helpers

static void RideRatingsSet(RatingTuple& ratings, int32_t excitement, int32_t intensity, int32_t nausea)
{
    ratings.excitement = 0;
    ratings.intensity = 0;
    ratings.nausea = 0;
    RideRatingsAdd(ratings, excitement, intensity, nausea);
}

/**
 * Add to a ride rating with overflow protection.
 */
static void RideRatingsAdd(RatingTuple& ratings, int32_t excitement, int32_t intensity, int32_t nausea)
{
    int32_t newExcitement = ratings.excitement + excitement;
    int32_t newIntensity = ratings.intensity + intensity;
    int32_t newNausea = ratings.nausea + nausea;
    ratings.excitement = std::clamp<int32_t>(newExcitement, 0, INT16_MAX);
    ratings.intensity = std::clamp<int32_t>(newIntensity, 0, INT16_MAX);
    ratings.nausea = std::clamp<int32_t>(newNausea, 0, INT16_MAX);
}

static void RideRatingsApplyBonusLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RideRatingsAdd(
        ratings, (std::min(ToHumanReadableRideLength(ride.GetTotalLength()), modifier.threshold) * modifier.excitement) >> 16,
        0, 0);
}

static void RideRatingsApplyBonusSynchronisation(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if ((ride.depart_flags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS) && RideHasAdjacentStation(ride))
    {
        RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
    }
}

static void RideRatingsApplyBonusTrainLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RideRatingsAdd(ratings, ((ride.num_cars_per_train - 1) * modifier.excitement) >> 16, 0, 0);
}

static void RideRatingsApplyBonusMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    int32_t maxSpeedMod = ride.max_speed >> 16;
    RideRatingsAdd(
        ratings, (maxSpeedMod * modifier.excitement) >> 16, (maxSpeedMod * modifier.intensity) >> 16,
        (maxSpeedMod * modifier.nausea) >> 16);
}

static void RideRatingsApplyBonusAverageSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    int32_t avgSpeedMod = ride.average_speed >> 16;
    RideRatingsAdd(ratings, (avgSpeedMod * modifier.excitement) >> 16, (avgSpeedMod * modifier.intensity) >> 16, 0);
}

static void RideRatingsApplyBonusDuration(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RideRatingsAdd(ratings, (std::min(ride.GetTotalTime(), modifier.threshold) * modifier.excitement) >> 16, 0, 0);
}

static void RideRatingsApplyBonusGForces(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RatingTuple subRating = ride_ratings_get_gforce_ratings(ride);
    RideRatingsAdd(
        ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
        (subRating.nausea * modifier.nausea) >> 16);
}

static void RideRatingsApplyBonusTurns(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RatingTuple subRating = ride_ratings_get_turns_ratings(ride);
    RideRatingsAdd(
        ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
        (subRating.nausea * modifier.nausea) >> 16);
}

static void RideRatingsApplyBonusDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RatingTuple subRating = ride_ratings_get_drop_ratings(ride);
    RideRatingsAdd(
        ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
        (subRating.nausea * modifier.nausea) >> 16);
}

static void RideRatingsApplyBonusSheltered(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RatingTuple subRating = ride_ratings_get_sheltered_ratings(ride);
    RideRatingsAdd(
        ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
        (subRating.nausea * modifier.nausea) >> 16);
}

static void RideRatingsApplyBonusRotations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RideRatingsAdd(
        ratings, ride.rotations * modifier.excitement, ride.rotations * modifier.intensity, ride.rotations * modifier.nausea);
}

static void RideRatingsApplyBonusOperationOption(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    int32_t intensity = (modifier.intensity >= 0) ? (ride.operation_option * modifier.intensity)
                                                  : (ride.operation_option / std::abs(modifier.intensity));
    RideRatingsAdd(ratings, ride.operation_option * modifier.excitement, intensity, ride.operation_option * modifier.nausea);
}

static void RideRatingsApplyBonusReversedTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.HasLifecycleFlag(RIDE_LIFECYCLE_REVERSED_TRAINS))
    {
        RideRatingsAdd(
            ratings, ((ratings.excitement * modifier.excitement) >> 7), (ratings.intensity * modifier.intensity) >> 7,
            (ratings.nausea * modifier.nausea) >> 7);
    }
}

static void RideRatingsApplyBonusGoKartRace(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.mode == RideMode::Race && ride.NumTrains >= modifier.threshold)
    {
        RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);

        int32_t lapsFactor = (ride.NumLaps - 1) * 30;
        RideRatingsAdd(ratings, lapsFactor, lapsFactor / 2, 0);
    }
}

static void RideRatingsApplyBonusTowerRide(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    int32_t lengthFactor = ToHumanReadableRideLength(ride.GetTotalLength());
    RideRatingsAdd(
        ratings, (lengthFactor * modifier.excitement) >> 16, (lengthFactor * modifier.intensity) >> 16,
        (lengthFactor * modifier.nausea) >> 16);
}

static void RideRatingsApplyBonusRotoDrop(RatingTuple& ratings, const Ride& ride)
{
    int32_t lengthFactor = (ToHumanReadableRideLength(ride.GetTotalLength()) * 209715) >> 16;
    RideRatingsAdd(ratings, lengthFactor, lengthFactor * 2, lengthFactor * 2);
}

static void RideRatingsApplyBonusMazeSize(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    int32_t size = std::min<uint16_t>(ride.maze_tiles, modifier.threshold);
    RideRatingsAdd(ratings, size * modifier.excitement, size * modifier.intensity, size * modifier.nausea);
}

static void RideRatingsApplyBonusBoatHireNoCircuit(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    // Most likely checking if the ride has does not have a circuit
    if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_TESTED))
    {
        RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
    }
}

static void RideRatingsApplyBonusSlideUnlimitedRides(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.mode == RideMode::UnlimitedRidesPerAdmission)
    {
        RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
    }
}

static void RideRatingsApplyBonusMotionSimulatorMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    // Hardcoded until ride mode refactor
    if (ride.mode == RideMode::FilmThrillRiders)
    {
        RideRatingsSet(ratings, MakeRideRating(3, 25), MakeRideRating(4, 10), MakeRideRating(3, 30));
    }
    else
    {
        RideRatingsSet(ratings, MakeRideRating(2, 90), MakeRideRating(3, 50), MakeRideRating(3, 00));
    }
}

static void RideRatingsApplyBonus3DCinemaMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    // Hardcoded until ride mode refactor
    switch (ride.mode)
    {
        default:
        case RideMode::MouseTails3DFilm:
            RideRatingsSet(ratings, MakeRideRating(3, 50), MakeRideRating(2, 40), MakeRideRating(1, 40));
            break;
        case RideMode::StormChasers3DFilm:
            RideRatingsSet(ratings, MakeRideRating(4, 00), MakeRideRating(2, 65), MakeRideRating(1, 55));
            break;
        case RideMode::SpaceRaiders3DFilm:
            RideRatingsSet(ratings, MakeRideRating(4, 20), MakeRideRating(2, 60), MakeRideRating(1, 48));
            break;
    }
}

static void RideRatingsApplyBonusTopSpinMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    // Hardcoded until ride mode refactor
    switch (ride.mode)
    {
        default:
        case RideMode::Beginners:
            RideRatingsSet(ratings, MakeRideRating(2, 00), MakeRideRating(4, 80), MakeRideRating(5, 74));
            break;
        case RideMode::Intense:
            RideRatingsSet(ratings, MakeRideRating(3, 00), MakeRideRating(5, 75), MakeRideRating(6, 64));
            break;
        case RideMode::Berserk:
            RideRatingsSet(ratings, MakeRideRating(3, 20), MakeRideRating(6, 80), MakeRideRating(7, 94));
            break;
    }
}

static void RideRatingsApplyBonusReversals(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
{
    int32_t numReversers = std::min<uint16_t>(state.AmountOfReversers, modifier.threshold);
    RideRatingsAdd(
        ratings, numReversers * modifier.excitement, numReversers * modifier.intensity, numReversers * modifier.nausea);
}

static void RideRatingsApplyBonusHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RideRatingsAdd(
        ratings, (ride.holes) * modifier.excitement, (ride.holes) * modifier.intensity, (ride.holes) * modifier.nausea);
}

static void RideRatingsApplyBonusNumTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    // For some reason the original code ran this twice, before and after the operation option bonus
    // Has been changed to call once with double value
    if (ride.NumTrains >= modifier.threshold)
    {
        RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
    }
}

static void RideRatingsApplyBonusDownwardLaunch(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.mode == RideMode::DownwardLaunch)
    {
        RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
    }
}

static void RideRatingsApplyBonusOperationOptionFreefall(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RideRatingsAdd(
        ratings, (ride.operation_option * modifier.excitement) >> 16, (ride.operation_option * modifier.intensity) >> 16,
        (ride.operation_option * modifier.nausea) >> 16);
}

static void RideRatingsApplyBonusLaunchedFreefallSpecial(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
{
    int32_t excitement = (ToHumanReadableRideLength(ride.GetTotalLength()) * 32768) >> 16;
    RideRatingsAdd(ratings, excitement, 0, 0);

#ifdef ORIGINAL_RATINGS
    RideRatingsApplyBonusOperationOptionFreefall(ratings, ride, modifier);
#else
    // Only apply "launch speed" effects when the setting can be modified
    if (ride.mode == RideMode::UpwardLaunch)
    {
        RideRatingsApplyBonusOperationOptionFreefall(ratings, ride, modifier);
    }
    else
    {
        // Fix #3282: When the ride mode is in downward launch mode, the intensity and
        //            nausea were fixed regardless of how high the ride is. The following
        //            calculation is based on roto-drop which is a similar mechanic.
        int32_t lengthFactor = (ToHumanReadableRideLength(ride.GetTotalLength()) * 209715) >> 16;
        RideRatingsAdd(ratings, lengthFactor, lengthFactor * 2, lengthFactor * 2);
    }
#endif
}

static void RideRatingsApplyBonusProximity(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
{
    RideRatingsAdd(ratings, (ride_ratings_get_proximity_score(state) * modifier.excitement) >> 16, 0, 0);
}

static void RideRatingsApplyBonusScenery(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    RideRatingsAdd(ratings, (ride_ratings_get_scenery_score(ride) * modifier.excitement) >> 16, 0, 0);
}

static void RideRatingsApplyRequirementLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.GetStation().SegmentLength < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementDropHeight(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.highest_drop_height < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.max_speed < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementNumDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.getNumDrops() < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementNegativeGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.max_negative_vertical_g >= modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.max_lateral_g < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementInversions(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.inversions < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementUnsheltered(
    RatingTuple& ratings, const Ride& ride, uint8_t shelteredEighths, RatingsModifier modifier)
{
    if (shelteredEighths >= modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementReversals(
    RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
{
    if (state.AmountOfReversers < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.holes < modifier.threshold)
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementStations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (ride.num_stations <= modifier.threshold)
    {
        // Excitement is set to 0 in original code - this could be changed for consistency
        ratings.excitement = 0;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

static void RideRatingsApplyRequirementSplashdown(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
    if (!(ride.special_track_elements & RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS))
    {
        ratings.excitement /= modifier.excitement;
        ratings.intensity /= modifier.intensity;
        ratings.nausea /= modifier.nausea;
    }
}

#ifndef ORIGINAL_RATINGS
static RatingTuple ride_ratings_get_excessive_lateral_g_penalty(const Ride& ride)
{
    RatingTuple result{};
    if (ride.max_lateral_g > MakeFixed16_2dp(2, 80))
    {
        result.intensity = MakeRideRating(3, 75);
        result.nausea = MakeRideRating(2, 00);
    }

    if (ride.max_lateral_g > MakeFixed16_2dp(3, 10))
    {
        // Remove half of the ride_ratings_get_gforce_ratings
        result.excitement = (ride.max_positive_vertical_g * 5242) >> 16;

        // Apply maximum negative G force factor
        fixed16_2dp gforce = ride.max_negative_vertical_g;
        result.excitement += (std::clamp<fixed16_2dp>(gforce, -MakeRideRating(2, 50), MakeRideRating(0, 00)) * -15728) >> 16;

        // Apply lateral G force factor
        result.excitement += (std::min<fixed16_2dp>(MakeRideRating(1, 50), ride.max_lateral_g) * 26214) >> 16;

        // Remove half of the ride_ratings_get_gforce_ratings
        result.excitement /= 2;
        result.excitement *= -1;
        result.intensity = MakeRideRating(12, 25);
        result.nausea = MakeRideRating(6, 00);
    }
    return result;
}
#endif

static void RideRatingsApplyPenaltyLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
{
#ifndef ORIGINAL_RATINGS
    RatingTuple subRating = ride_ratings_get_excessive_lateral_g_penalty(ride);
    RideRatingsAdd(
        ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
        (subRating.nausea * modifier.nausea) >> 16);
#endif
}

#pragma endregion

bool RatingTuple::isNull() const
{
    return excitement == kRideRatingUndefined;
}

void RatingTuple::setNull()
{
    excitement = kRideRatingUndefined;
}
