/*****************************************************************************
 * 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 "Peep.h"

#include "../Cheats.h"
#include "../Context.h"
#include "../Diagnostic.h"
#include "../Game.h"
#include "../GameState.h"
#include "../Input.h"
#include "../OpenRCT2.h"
#include "../SpriteIds.h"
#include "../actions/GameAction.hpp"
#include "../audio/Audio.h"
#include "../audio/AudioChannel.h"
#include "../audio/AudioMixer.h"
#include "../config/Config.h"
#include "../core/EnumUtils.hpp"
#include "../core/Guard.hpp"
#include "../core/String.hpp"
#include "../drawing/Drawing.h"
#include "../entity/Balloon.h"
#include "../entity/EntityList.h"
#include "../entity/EntityRegistry.h"
#include "../entity/EntityTweener.h"
#include "../interface/Viewport.h"
#include "../interface/WindowBase.h"
#include "../localisation/Formatter.h"
#include "../localisation/Formatting.h"
#include "../management/Finance.h"
#include "../management/Marketing.h"
#include "../management/NewsItem.h"
#include "../network/Network.h"
#include "../object/ObjectManager.h"
#include "../object/PeepAnimationsObject.h"
#include "../peep/GuestPathfinding.h"
#include "../profiling/Profiling.h"
#include "../ride/Ride.h"
#include "../ride/RideData.h"
#include "../ride/ShopItem.h"
#include "../ride/Station.h"
#include "../scenario/Scenario.h"
#include "../ui/WindowManager.h"
#include "../util/Util.h"
#include "../windows/Intent.h"
#include "../world/ConstructionClearance.h"
#include "../world/Entrance.h"
#include "../world/Footpath.h"
#include "../world/Map.h"
#include "../world/Park.h"
#include "../world/QuarterTile.h"
#include "../world/Scenery.h"
#include "../world/Weather.h"
#include "../world/tile_element/EntranceElement.h"
#include "../world/tile_element/PathElement.h"
#include "../world/tile_element/SurfaceElement.h"
#include "../world/tile_element/TrackElement.h"
#include "PatrolArea.h"
#include "Staff.h"

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

namespace OpenRCT2
{
    using namespace OpenRCT2::Audio;
    using namespace OpenRCT2::Drawing;

    static uint8_t _backupAnimationImageIdOffset;
    static TileElement* _peepRideEntranceExitElement;

    static std::shared_ptr<IAudioChannel> _crowdSoundChannel = nullptr;

    static void GuestReleaseBalloon(Guest* peep, int16_t spawn_height);

    static PeepAnimationType PeepSpecialSpriteToAnimationGroupMap[] = {
        PeepAnimationType::walking,
        PeepAnimationType::holdMat,
        PeepAnimationType::staffMower,
    };

    static PeepAnimationType PeepActionToAnimationGroupMap[] = {
        PeepAnimationType::checkTime,
        PeepAnimationType::eatFood,
        PeepAnimationType::shakeHead,
        PeepAnimationType::emptyPockets,
        PeepAnimationType::sittingEatFood,
        PeepAnimationType::sittingLookAroundLeft,
        PeepAnimationType::sittingLookAroundRight,
        PeepAnimationType::wow,
        PeepAnimationType::throwUp,
        PeepAnimationType::jump,
        PeepAnimationType::staffSweep,
        PeepAnimationType::drowning,
        PeepAnimationType::staffAnswerCall,
        PeepAnimationType::staffAnswerCall2,
        PeepAnimationType::staffCheckBoard,
        PeepAnimationType::staffFix,
        PeepAnimationType::staffFix2,
        PeepAnimationType::staffFixGround,
        PeepAnimationType::staffFix3,
        PeepAnimationType::staffWatering,
        PeepAnimationType::joy,
        PeepAnimationType::readMap,
        PeepAnimationType::wave,
        PeepAnimationType::staffEmptyBin,
        PeepAnimationType::wave2,
        PeepAnimationType::takePhoto,
        PeepAnimationType::clap,
        PeepAnimationType::disgust,
        PeepAnimationType::drawPicture,
        PeepAnimationType::beingWatched,
        PeepAnimationType::withdrawMoney,
    };

    template<>
    bool EntityBase::is<Peep>() const
    {
        return type == EntityType::guest || type == EntityType::staff;
    }

    uint8_t Peep::GetNextDirection() const
    {
        return NextFlags & PEEP_NEXT_FLAG_DIRECTION_MASK;
    }

    bool Peep::GetNextIsSloped() const
    {
        return NextFlags & PEEP_NEXT_FLAG_IS_SLOPED;
    }

    bool Peep::GetNextIsSurface() const
    {
        return NextFlags & PEEP_NEXT_FLAG_IS_SURFACE;
    }

    void Peep::SetNextFlags(uint8_t next_direction, bool is_sloped, bool is_surface)
    {
        NextFlags = next_direction & PEEP_NEXT_FLAG_DIRECTION_MASK;
        NextFlags |= is_sloped ? PEEP_NEXT_FLAG_IS_SLOPED : 0;
        NextFlags |= is_surface ? PEEP_NEXT_FLAG_IS_SURFACE : 0;
    }

    bool Peep::CanBePickedUp() const
    {
        switch (State)
        {
            case PeepState::one:
            case PeepState::queuingFront:
            case PeepState::onRide:
            case PeepState::enteringRide:
            case PeepState::leavingRide:
            case PeepState::enteringPark:
            case PeepState::leavingPark:
            case PeepState::fixing:
            case PeepState::buying:
            case PeepState::inspecting:
                return false;
            case PeepState::falling:
            case PeepState::walking:
            case PeepState::queuing:
            case PeepState::sitting:
            case PeepState::picked:
            case PeepState::patrolling:
            case PeepState::mowing:
            case PeepState::sweeping:
            case PeepState::answering:
            case PeepState::watching:
            case PeepState::emptyingBin:
            case PeepState::usingBin:
            case PeepState::watering:
            case PeepState::headingToInspection:
                return true;
        }
        return false;
    }

    int32_t PeepGetStaffCount()
    {
        return getGameState().entities.GetEntityListCount(EntityType::staff);
    }

    /**
     *
     *  rct2: 0x0068F0A9
     */
    void PeepUpdateAll()
    {
        PROFILED_FUNCTION();

        if (isInEditorMode())
            return;

        const auto currentTicks = getGameState().currentTicks;

        constexpr auto kTicks128Mask = 128u - 1u;
        const auto currentTicksMasked = currentTicks & kTicks128Mask;

        uint32_t index = 0;

        for (auto peep : EntityList<Guest>())
        {
            if ((index & kTicks128Mask) == currentTicksMasked)
            {
                peep->tick128UpdateGuest(index);
            }

            peep->update();

            index++;
        }

        for (auto staff : EntityList<Staff>())
        {
            if ((index & kTicks128Mask) == currentTicksMasked)
            {
                staff->tick128UpdateStaff();
            }

            staff->Update();

            index++;
        }
    }

    void PeepUpdateAllBoundingBoxes()
    {
        for (auto* peep : EntityList<Guest>())
        {
            peep->UpdateSpriteBoundingBox();
        }

        for (auto* peep : EntityList<Staff>())
        {
            peep->UpdateSpriteBoundingBox();
        }
    }

    void Peep::UpdateWaitingAtCrossing()
    {
        if (!IsActionInterruptable())
        {
            UpdateAction();
            invalidate();
            if (!IsActionWalking())
                return;
        }

        Action = PeepActionType::idle;
        NextAnimationType = PeepAnimationType::watchRide;
        SwitchNextAnimationType();

        auto* guest = as<Guest>();
        if (guest != nullptr)
        {
            if (guest->IsActionInterruptable())
            {
                if (guest->hasFoodOrDrink())
                {
                    if ((ScenarioRand() & 0xFFFF) <= 1310)
                    {
                        Action = PeepActionType::eatFood;
                        AnimationFrameNum = 0;
                        AnimationImageIdOffset = 0;
                    }
                }
                else if ((ScenarioRand() & 0xFFFF) <= 64)
                {
                    Action = PeepActionType::wave2;
                    AnimationFrameNum = 0;
                    AnimationImageIdOffset = 0;
                }
            }
        }

        UpdateCurrentAnimationType();
    }

    /*
     * rct2: 0x68F3AE
     * Set peep state to falling if path below has gone missing, return true if current path is valid, false if peep starts
     * falling.
     */
    bool Peep::CheckForPath()
    {
        PROFILED_FUNCTION();

        PathCheckOptimisation++;
        if ((PathCheckOptimisation & 0xF) != (id.ToUnderlying() & 0xF))
        {
            // This condition makes the check happen less often
            // As a side effect peeps hover for a short,
            // random time when a path below them has been deleted
            return true;
        }

        TileElement* tile_element = MapGetFirstElementAt(NextLoc);

        auto mapType = TileElementType::Path;
        if (GetNextIsSurface())
        {
            mapType = TileElementType::Surface;
        }

        do
        {
            if (tile_element == nullptr)
                break;
            if (tile_element->getType() == mapType)
            {
                if (NextLoc.z == tile_element->getBaseZ())
                {
                    // Found a suitable path or surface
                    return true;
                }
            }
        } while (!(tile_element++)->isLastForTile());

        // Found no suitable path
        SetState(PeepState::falling);
        return false;
    }

    bool Peep::ShouldWaitForLevelCrossing() const
    {
        if (IsOnPathBlockedByVehicle())
        {
            // Try to get out of the way
            return false;
        }

        auto curPos = TileCoordsXYZ(getLocation());
        auto dstPos = TileCoordsXYZ(CoordsXYZ{ GetDestination(), NextLoc.z });
        if ((curPos.x != dstPos.x || curPos.y != dstPos.y) && FootpathIsBlockedByVehicle(dstPos))
        {
            return true;
        }

        return false;
    }

    bool Peep::IsOnLevelCrossing() const
    {
        auto loc = getLocation();
        auto pathElement = MapGetFootpathElement(loc);
        if (pathElement != nullptr)
        {
            return pathElement->asPath()->IsLevelCrossing(loc);
        }
        return false;
    }

    bool Peep::IsOnPathBlockedByVehicle() const
    {
        auto curPos = TileCoordsXYZ(getLocation());
        return FootpathIsBlockedByVehicle(curPos);
    }

    PeepAnimationType Peep::GetAnimationType()
    {
        if (IsActionInterruptable())
        { // PeepActionType::none1 or PeepActionType::none2
            return PeepSpecialSpriteToAnimationGroupMap[SpecialSprite];
        }

        if (EnumValue(Action) < std::size(PeepActionToAnimationGroupMap))
        {
            return PeepActionToAnimationGroupMap[EnumValue(Action)];
        }

        Guard::Assert(
            EnumValue(Action) >= std::size(PeepActionToAnimationGroupMap) && Action < PeepActionType::idle,
            "Invalid peep action %u", EnumValue(Action));
        return PeepAnimationType::walking;
    }

    /*
     *  rct2: 0x00693B58
     */
    void Peep::UpdateCurrentAnimationType()
    {
        PeepAnimationType newAnimationType = GetAnimationType();
        if (AnimationType == newAnimationType)
        {
            return;
        }

        AnimationType = newAnimationType;

        invalidate();
        UpdateSpriteBoundingBox();
        invalidate();
    }

    void Peep::UpdateSpriteBoundingBox()
    {
        auto& objManager = GetContext()->GetObjectManager();
        auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);

        const auto& spriteBounds = animObj->GetSpriteBounds(AnimationGroup, AnimationType);
        spriteData.width = spriteBounds.spriteWidth;
        spriteData.heightMin = spriteBounds.spriteHeightNegative;
        spriteData.heightMax = spriteBounds.spriteHeightPositive;
    }

    /* rct2: 0x00693BE5 */
    void Peep::SwitchToSpecialSprite(uint8_t special_sprite_id)
    {
        if (special_sprite_id == SpecialSprite)
            return;

        SpecialSprite = special_sprite_id;

        if (IsActionInterruptable())
        {
            AnimationImageIdOffset = 0;
        }
        UpdateCurrentAnimationType();
    }

    void Peep::StateReset()
    {
        SetState(PeepState::one);
        SwitchToSpecialSprite(0);
    }

    /** rct2: 0x00981D7C, 0x00981D7E */
    static constexpr CoordsXY kWalkingOffsetByDirection[kNumOrthogonalDirections] = {
        { -2, 0 },
        { 0, 2 },
        { 2, 0 },
        { 0, -2 },
    };

    std::optional<CoordsXY> Peep::UpdateAction()
    {
        int16_t xy_distance;
        return UpdateAction(xy_distance);
    }

    /**
     *
     *  rct2: 0x6939EB
     * Also used to move peeps to the correct position to
     * start an action. Returns true if the correct destination
     * has not yet been reached. xy_distance is how close the
     * peep is to the target.
     */
    std::optional<CoordsXY> Peep::UpdateAction(int16_t& xy_distance)
    {
        PROFILED_FUNCTION();

        _backupAnimationImageIdOffset = AnimationImageIdOffset;
        if (Action == PeepActionType::idle)
        {
            Action = PeepActionType::walking;
        }

        CoordsXY differenceLoc = getLocation();
        differenceLoc -= GetDestination();

        int32_t x_delta = abs(differenceLoc.x);
        int32_t y_delta = abs(differenceLoc.y);

        xy_distance = x_delta + y_delta;

        // We're taking an easier route if we're just walking
        if (IsActionWalking())
        {
            return UpdateWalkingAction(differenceLoc, xy_distance);
        }

        if (!UpdateActionAnimation())
        {
            AnimationImageIdOffset = 0;
            Action = PeepActionType::walking;
            UpdateCurrentAnimationType();
            return { { x, y } };
        }

        // Should we throw up, and are we at the frame where sick appears?
        if (auto* guest = as<Guest>(); guest != nullptr)
        {
            if (Action == PeepActionType::throwUp && AnimationFrameNum == 15)
            {
                guest->throwUp();
            }
        }

        return { { x, y } };
    }

    bool Peep::UpdateActionAnimation()
    {
        auto& objManager = GetContext()->GetObjectManager();
        auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);

        const PeepAnimation& peepAnimation = animObj->GetPeepAnimation(AnimationGroup, AnimationType);
        AnimationFrameNum++;

        // If last frame of action
        if (AnimationFrameNum >= peepAnimation.frameOffsets.size())
        {
            return false;
        }

        AnimationImageIdOffset = peepAnimation.frameOffsets[AnimationFrameNum];
        return true;
    }

    std::optional<CoordsXY> Peep::UpdateWalkingAction(const CoordsXY& differenceLoc, int16_t& xy_distance)
    {
        if (!IsActionWalking())
        {
            return std::nullopt;
        }

        if (xy_distance <= DestinationTolerance)
        {
            return std::nullopt;
        }

        int32_t x_delta = abs(differenceLoc.x);
        int32_t y_delta = abs(differenceLoc.y);

        int32_t nextDirection = 0;
        if (x_delta < y_delta)
        {
            nextDirection = 1;
            if (differenceLoc.y >= 0)
            {
                nextDirection = 3;
            }
        }
        else
        {
            nextDirection = 2;
            if (differenceLoc.x >= 0)
            {
                nextDirection = 0;
            }
        }

        orientation = nextDirection * 8;

        CoordsXY loc = { x, y };
        loc += kWalkingOffsetByDirection[nextDirection];

        UpdateWalkingAnimation();

        return loc;
    }

    void Peep::UpdateWalkingAnimation()
    {
        auto& objManager = GetContext()->GetObjectManager();
        auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);

        WalkingAnimationFrameNum++;
        const PeepAnimation& peepAnimation = animObj->GetPeepAnimation(AnimationGroup, AnimationType);
        if (WalkingAnimationFrameNum >= peepAnimation.frameOffsets.size())
        {
            WalkingAnimationFrameNum = 0;
        }
        AnimationImageIdOffset = peepAnimation.frameOffsets[WalkingAnimationFrameNum];
    }

    /**
     *  rct2: 0x0069A409
     * Decreases rider count if on/entering a ride.
     */
    void PeepDecrementNumRiders(Peep* peep)
    {
        if (peep->State == PeepState::onRide || peep->State == PeepState::enteringRide)
        {
            auto ride = GetRide(peep->CurrentRide);
            if (ride != nullptr)
            {
                ride->numRiders = std::max(0, ride->numRiders - 1);
                ride->windowInvalidateFlags.set(RideInvalidateFlag::main, RideInvalidateFlag::list);
            }
        }
    }

    /**
     * Call after changing a peeps state to insure that all relevant windows update.
     * Note also increase ride count if on/entering a ride.
     *  rct2: 0x0069A42F
     */
    void PeepWindowStateUpdate(Peep* peep)
    {
        auto* windowMgr = Ui::GetWindowManager();
        WindowBase* w = windowMgr->FindByNumber(WindowClass::peep, peep->id.ToUnderlying());
        if (w != nullptr)
            w->onPrepareDraw();

        if (peep->is<Guest>())
        {
            if (peep->State == PeepState::onRide || peep->State == PeepState::enteringRide)
            {
                auto ride = GetRide(peep->CurrentRide);
                if (ride != nullptr)
                {
                    ride->numRiders++;
                    ride->windowInvalidateFlags.set(RideInvalidateFlag::main, RideInvalidateFlag::list);
                }
            }

            windowMgr->InvalidateByNumber(WindowClass::peep, peep->id);
            windowMgr->InvalidateByClass(WindowClass::guestList);
        }
        else
        {
            windowMgr->InvalidateByNumber(WindowClass::peep, peep->id);
            windowMgr->InvalidateByClass(WindowClass::staffList);
        }
    }

    void Peep::Pickup()
    {
        if (auto* guest = as<Guest>(); guest != nullptr)
        {
            guest->RemoveFromRide();
        }
        moveTo({ kLocationNull, y, z });
        SetState(PeepState::picked);
        SubState = 0;
    }

    void Peep::PickupAbort(int32_t old_x)
    {
        if (State != PeepState::picked)
            return;

        moveTo({ old_x, y, z + 8 });

        if (x != kLocationNull)
        {
            SetState(PeepState::falling);
            Action = PeepActionType::walking;
            SpecialSprite = 0;
            AnimationImageIdOffset = 0;
            AnimationType = PeepAnimationType::walking;
            PathCheckOptimisation = 0;
        }

        gPickupPeepImage = ImageId();
    }

    // Returns GameActions::Status::ok when a peep can be dropped at the given location. When apply is set to true the peep gets
    // dropped.
    GameActions::Result Peep::Place(const TileCoordsXYZ& location, bool apply)
    {
        auto* pathElement = MapGetPathElementAt(location);
        TileElement* tileElement = reinterpret_cast<TileElement*>(pathElement);
        if (pathElement == nullptr)
        {
            tileElement = reinterpret_cast<TileElement*>(MapGetSurfaceElementAt(location));
        }
        if (tileElement == nullptr)
        {
            return GameActions::Result(GameActions::Status::invalidParameters, STR_ERR_CANT_PLACE_PERSON_HERE, kStringIdNone);
        }

        // Set the coordinate of destination to be exactly
        // in the middle of a tile.
        CoordsXYZ destination = { location.ToCoordsXY().ToTileCentre(), tileElement->getBaseZ() + 16 };

        if (!MapIsLocationOwned(destination))
        {
            return GameActions::Result(
                GameActions::Status::notOwned, STR_ERR_CANT_PLACE_PERSON_HERE, STR_LAND_NOT_OWNED_BY_PARK);
        }

        if (auto res = MapCanConstructAt({ destination, destination.z, destination.z + (1 * 8) }, { 0b1111, 0 });
            res.error != GameActions::Status::ok)
        {
            const auto stringId = std::get<StringId>(res.errorMessage);
            if (stringId != STR_RAISE_OR_LOWER_LAND_FIRST && stringId != STR_FOOTPATH_IN_THE_WAY)
            {
                return GameActions::Result(
                    GameActions::Status::noClearance, STR_ERR_CANT_PLACE_PERSON_HERE, stringId, res.errorMessageArgs.data());
            }
        }

        if (apply)
        {
            moveTo(destination);
            SetState(PeepState::falling);
            Action = PeepActionType::walking;
            SpecialSprite = 0;
            AnimationImageIdOffset = 0;
            AnimationType = PeepAnimationType::walking;
            PathCheckOptimisation = 0;
            EntityTweener::Get().Reset();
            if (auto* guest = as<Guest>(); guest != nullptr)
            {
                AnimationType = PeepAnimationType::invalid;
                guest->happinessTarget = std::max(guest->happinessTarget - 10, 0);
                UpdateCurrentAnimationType();
            }
        }

        return GameActions::Result();
    }

    /**
     *
     *  rct2: 0x0069A535
     */
    void PeepEntityRemove(Peep* peep)
    {
        auto* guest = peep->as<Guest>();
        if (guest != nullptr)
        {
            guest->RemoveFromRide();
        }
        peep->invalidate();

        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->CloseByNumber(WindowClass::peep, peep->id);
        windowMgr->CloseByNumber(WindowClass::firePrompt, EnumValue(peep->type));

        auto* staff = peep->as<Staff>();
        // Needed for invalidations after sprite removal
        bool wasGuest = staff == nullptr;
        if (wasGuest)
        {
            News::DisableNewsItems(News::ItemType::peepOnRide, peep->id.ToUnderlying());
        }
        else
        {
            staff->clearPatrolArea();
            UpdateConsolidatedPatrolAreas();

            News::DisableNewsItems(News::ItemType::peep, staff->id.ToUnderlying());
        }
        getGameState().entities.EntityRemove(peep);

        auto intent = Intent(wasGuest ? INTENT_ACTION_REFRESH_GUEST_LIST : INTENT_ACTION_REFRESH_STAFF_LIST);
        ContextBroadcastIntent(&intent);
    }

    /**
     * New function removes peep from park existence. Works with staff.
     */
    void Peep::Remove()
    {
        auto* guest = as<Guest>();
        if (guest != nullptr)
        {
            if (!guest->outsideOfPark)
            {
                DecrementGuestsInPark();
                auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT);
                ContextBroadcastIntent(&intent);
            }
            if (State == PeepState::enteringPark)
            {
                DecrementGuestsHeadingForPark();
            }
        }
        PeepEntityRemove(this);
    }

    /**
     * Falling and its subset drowning
     *  rct2: 0x690028
     */
    void Peep::UpdateFalling()
    {
        if (Action == PeepActionType::drowning)
        {
            // Check to see if we are ready to drown.
            UpdateAction();
            invalidate();
            if (Action == PeepActionType::drowning)
                return;

            if (Config::Get().notifications.guestDied)
            {
                auto ft = Formatter();
                FormatNameTo(ft);
                News::AddItemToQueue(News::ItemType::blank, STR_NEWS_ITEM_GUEST_DROWNED, x | (y << 16), ft);
            }

            auto& gameState = getGameState();
            gameState.park.ratingCasualtyPenalty = std::min(gameState.park.ratingCasualtyPenalty + 25, 1000);
            Remove();
            return;
        }

        // If not drowning then falling. Note: peeps 'fall' after leaving a ride/enter the park.
        TileElement* tile_element = MapGetFirstElementAt(CoordsXY{ x, y });
        TileElement* saved_map = nullptr;
        int32_t saved_height = 0;

        if (tile_element != nullptr)
        {
            do
            {
                // If a path check if we are on it
                if (tile_element->getType() == TileElementType::Path)
                {
                    int32_t height = MapHeightFromSlope(
                                         { x, y }, tile_element->asPath()->GetSlopeDirection(),
                                         tile_element->asPath()->IsSloped())
                        + tile_element->getBaseZ();

                    if (height < z - 1 || height > z + 8)
                        continue;

                    saved_height = height;
                    saved_map = tile_element;
                    break;
                } // If a surface get the height and see if we are on it
                else if (tile_element->getType() == TileElementType::Surface)
                {
                    // If the surface is water check to see if we could be drowning
                    if (tile_element->asSurface()->GetWaterHeight() > 0)
                    {
                        int32_t height = tile_element->asSurface()->GetWaterHeight();

                        if (height - 4 >= z && height < z + 20)
                        {
                            // Looks like we are drowning!
                            moveTo({ x, y, height });

                            if (auto* guest = as<Guest>(); guest != nullptr)
                            {
                                // Drop balloon if held
                                GuestReleaseBalloon(guest, height);
                                guest->insertNewThought(PeepThoughtType::drowning);
                            }

                            Action = PeepActionType::drowning;
                            AnimationFrameNum = 0;
                            AnimationImageIdOffset = 0;

                            UpdateCurrentAnimationType();
                            PeepWindowStateUpdate(this);
                            return;
                        }
                    }
                    int32_t map_height = TileElementHeight({ x, y });
                    if (map_height < z || map_height - 4 > z)
                        continue;
                    saved_height = map_height;
                    saved_map = tile_element;
                } // If not a path or surface go see next element
            } while (!(tile_element++)->isLastForTile());
        }

        // This will be null if peep is falling
        if (saved_map == nullptr)
        {
            if (z <= 1)
            {
                // Remove peep if it has gone to the void
                Remove();
                return;
            }
            moveTo({ x, y, z - 2 });
            return;
        }

        moveTo({ x, y, saved_height });

        NextLoc = { CoordsXY{ x, y }.ToTileStart(), saved_map->getBaseZ() };

        if (saved_map->getType() != TileElementType::Path)
        {
            SetNextFlags(0, false, true);
        }
        else
        {
            SetNextFlags(saved_map->asPath()->GetSlopeDirection(), saved_map->asPath()->IsSloped(), false);
        }
        SetState(PeepState::one);
    }

    /**
     *
     *  rct2: 0x6902A2
     */
    void Peep::Update1()
    {
        if (!CheckForPath())
            return;

        if (is<Guest>())
        {
            SetState(PeepState::walking);
        }
        else
        {
            SetState(PeepState::patrolling);
        }

        SetDestination(getLocation(), 10);
        PeepDirection = orientation >> 3;
    }

    void Peep::SetState(PeepState new_state)
    {
        PeepDecrementNumRiders(this);
        State = new_state;
        PeepWindowStateUpdate(this);
    }

    /**
     *
     *  rct2: 0x690009
     */
    void Peep::UpdatePicked()
    {
        if (getGameState().currentTicks & 0x1F)
            return;
        SubState++;
        auto* guest = as<Guest>();
        if (SubState == 13 && guest != nullptr)
        {
            guest->insertNewThought(PeepThoughtType::help);
        }
    }

    uint32_t Peep::GetStepsToTake() const
    {
        uint32_t stepsToTake = Energy;
        if (stepsToTake < 95 && State == PeepState::queuing)
            stepsToTake = 95;
        if ((PeepFlags & PEEP_FLAGS_SLOW_WALK) && State != PeepState::queuing)
            stepsToTake /= 2;
        if (IsActionWalking() && GetNextIsSloped())
        {
            stepsToTake /= 2;
            if (State == PeepState::queuing)
                stepsToTake += stepsToTake / 2;
        }
        // Ensure guests make it across a level crossing in time
        constexpr auto minStepsForCrossing = 55;
        if (stepsToTake < minStepsForCrossing && IsOnPathBlockedByVehicle())
            stepsToTake = minStepsForCrossing;

        return stepsToTake;
    }

    /**
     *
     *  rct2: 0x0069BF41
     */
    void PeepProblemWarningsUpdate()
    {
        auto& gameState = getGameState();

        Ride* ride;
        uint32_t hungerCounter = 0, lostCounter = 0, noexitCounter = 0, thirstCounter = 0, litterCounter = 0,
                 disgustCounter = 0, toiletCounter = 0, vandalismCounter = 0;
        uint8_t* warningThrottle = gameState.park.peepWarningThrottle;

        int32_t inQueueCounter = 0;
        int32_t tooLongQueueCounter = 0;
        std::map<RideId, int32_t> queueComplainingGuestsMap;

        for (auto peep : EntityList<Guest>())
        {
            if (peep->outsideOfPark)
                continue;

            if (peep->State == PeepState::queuing || peep->State == PeepState::queuingFront)
                inQueueCounter++;

            if (peep->thoughts[0].freshness > 5)
                continue;

            switch (peep->thoughts[0].type)
            {
                case PeepThoughtType::lost: // 0x10
                    lostCounter++;
                    break;

                case PeepThoughtType::hungry: // 0x14
                    if (peep->guestHeadingToRideId.IsNull())
                    {
                        hungerCounter++;
                        break;
                    }
                    ride = GetRide(peep->guestHeadingToRideId);
                    if (ride != nullptr && !ride->getRideTypeDescriptor().flags.has(RtdFlag::sellsFood))
                        hungerCounter++;
                    break;

                case PeepThoughtType::thirsty:
                    if (peep->guestHeadingToRideId.IsNull())
                    {
                        thirstCounter++;
                        break;
                    }
                    ride = GetRide(peep->guestHeadingToRideId);
                    if (ride != nullptr && !ride->getRideTypeDescriptor().flags.has(RtdFlag::sellsDrinks))
                        thirstCounter++;
                    break;

                case PeepThoughtType::toilet:
                    if (peep->guestHeadingToRideId.IsNull())
                    {
                        toiletCounter++;
                        break;
                    }
                    ride = GetRide(peep->guestHeadingToRideId);
                    if (ride != nullptr && ride->getRideTypeDescriptor().specialType != RtdSpecialType::toilet)
                        toiletCounter++;
                    break;

                case PeepThoughtType::badLitter: // 0x1a
                    litterCounter++;
                    break;
                case PeepThoughtType::cantFindExit: // 0x1b
                    noexitCounter++;
                    break;
                case PeepThoughtType::pathDisgusting: // 0x1f
                    disgustCounter++;
                    break;
                case PeepThoughtType::vandalism: // 0x21
                    vandalismCounter++;
                    break;
                case PeepThoughtType::queuingAges:
                    tooLongQueueCounter++;
                    queueComplainingGuestsMap[peep->thoughts[0].rideId]++;
                    break;
                default:
                    break;
            }
        }

        // could maybe be packed into a loop, would lose a lot of clarity though
        if (warningThrottle[0])
            --warningThrottle[0];
        else if (hungerCounter >= kPeepHungerWarningThreshold && hungerCounter >= gameState.park.numGuestsInPark / 16)
        {
            warningThrottle[0] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::hungry);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_ARE_HUNGRY, kThoughtId, {});
            }
        }

        if (warningThrottle[1])
            --warningThrottle[1];
        else if (thirstCounter >= kPeepThirstWarningThreshold && thirstCounter >= gameState.park.numGuestsInPark / 16)
        {
            warningThrottle[1] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::thirsty);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_ARE_THIRSTY, kThoughtId, {});
            }
        }

        if (warningThrottle[2])
            --warningThrottle[2];
        else if (toiletCounter >= kPeepToiletWarningThreshold && toiletCounter >= gameState.park.numGuestsInPark / 16)
        {
            warningThrottle[2] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::toilet);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_CANT_FIND_TOILET, kThoughtId, {});
            }
        }

        if (warningThrottle[3])
            --warningThrottle[3];
        else if (litterCounter >= kPeepLitterWarningThreshold && litterCounter >= gameState.park.numGuestsInPark / 32)
        {
            warningThrottle[3] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::badLitter);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_DISLIKE_LITTER, kThoughtId, {});
            }
        }

        if (warningThrottle[4])
            --warningThrottle[4];
        else if (disgustCounter >= kPeepDisgustWarningThreshold && disgustCounter >= gameState.park.numGuestsInPark / 32)
        {
            warningThrottle[4] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::pathDisgusting);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_DISGUSTED_BY_PATHS, kThoughtId, {});
            }
        }

        if (warningThrottle[5])
            --warningThrottle[5];
        else if (vandalismCounter >= kPeepVandalismWarningThreshold && vandalismCounter >= gameState.park.numGuestsInPark / 32)
        {
            warningThrottle[5] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::vandalism);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_DISLIKE_VANDALISM, kThoughtId, {});
            }
        }

        if (warningThrottle[6])
            --warningThrottle[6];
        else if (noexitCounter >= kPeepNoExitWarningThreshold)
        {
            warningThrottle[6] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::cantFindExit);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_GETTING_LOST_OR_STUCK, kThoughtId, {});
            }
        }
        else if (lostCounter >= kPeepLostWarningThreshold)
        {
            warningThrottle[6] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                constexpr auto kThoughtId = static_cast<uint32_t>(PeepThoughtType::lost);
                News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_GETTING_LOST_OR_STUCK, kThoughtId, {});
            }
        }

        if (warningThrottle[7])
            --warningThrottle[7];
        else if (tooLongQueueCounter > kPeepTooLongQueueThreshold && tooLongQueueCounter > inQueueCounter / 20)
        { // The amount of guests complaining about queue duration is at least 5% of the amount of queuing guests.
          // This includes guests who are no longer queuing.
            warningThrottle[7] = 4;
            if (Config::Get().notifications.guestWarnings)
            {
                auto rideWithMostQueueComplaints = std::max_element(
                    queueComplainingGuestsMap.begin(), queueComplainingGuestsMap.end(),
                    [](auto& lhs, auto& rhs) { return lhs.second < rhs.second; });
                auto rideId = rideWithMostQueueComplaints->first.ToUnderlying();
                News::AddItemToQueue(News::ItemType::ride, STR_PEEPS_COMPLAINING_ABOUT_QUEUE_LENGTH_WARNING, rideId, {});
            }
        }
    }

    void PeepStopCrowdNoise()
    {
        if (_crowdSoundChannel != nullptr)
        {
            _crowdSoundChannel->Stop();
            _crowdSoundChannel = nullptr;
        }
    }

    /**
     *
     *  rct2: 0x006BD18A
     */
    void PeepUpdateCrowdNoise()
    {
        PROFILED_FUNCTION();

        if (gGameSoundsOff)
            return;

        if (!Config::Get().sound.soundEnabled)
            return;

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

        auto viewport = gMusicTrackingViewport;
        if (viewport == nullptr)
            return;

        // Count the number of peeps visible
        auto visiblePeeps = 0;

        for (auto peep : EntityList<Guest>())
        {
            if (peep->x == kLocationNull)
                continue;
            if (viewport->viewPos.x > peep->spriteData.spriteRect.GetRight())
                continue;
            if (viewport->viewPos.x + viewport->ViewWidth() < peep->spriteData.spriteRect.GetLeft())
                continue;
            if (viewport->viewPos.y > peep->spriteData.spriteRect.GetBottom())
                continue;
            if (viewport->viewPos.y + viewport->ViewHeight() < peep->spriteData.spriteRect.GetTop())
                continue;

            visiblePeeps += peep->State == PeepState::queuing ? 1 : 2;
        }

        // This function doesn't account for the fact that the screen might be so big that 100 peeps could potentially be very
        // spread out and therefore not produce any crowd noise. Perhaps a more sophisticated solution would check how many
        // peeps were in close proximity to each other.

        // Allows queuing peeps to make half as much noise, and at least 6 peeps must be visible for any crowd noise
        visiblePeeps = (visiblePeeps / 2) - 6;
        if (visiblePeeps < 0)
        {
            // Mute crowd noise
            if (_crowdSoundChannel != nullptr)
            {
                _crowdSoundChannel->SetVolume(0);
            }
        }
        else
        {
            // Formula to scale peeps to dB where peeps [0, 120] scales approximately logarithmically to [-3314, -150] dB/100
            // 207360000 maybe related to DSBVOLUME_MIN which is -10,000 (dB/100)
            int32_t volume = 120 - std::min(visiblePeeps, 120);
            volume = volume * volume * volume * volume;
            volume = (viewport->zoom.ApplyInversedTo(207360000 - volume) - 207360000) / 65536 - 150;

            // Load and play crowd noise if needed and set volume
            if (_crowdSoundChannel == nullptr || _crowdSoundChannel->IsDone())
            {
                _crowdSoundChannel = CreateAudioChannel(SoundId::crowdAmbience, true, 0);
                if (_crowdSoundChannel != nullptr)
                {
                    _crowdSoundChannel->SetGroup(MixerGroup::Sound);
                }
            }
            if (_crowdSoundChannel != nullptr)
            {
                _crowdSoundChannel->SetVolume(DStoMixerVolume(volume));
            }
        }
    }

    /**
     *
     *  rct2: 0x0069BE9B
     */
    void PeepApplause()
    {
        for (auto peep : EntityList<Guest>())
        {
            if (peep->outsideOfPark || peep->PeepFlags & PEEP_FLAGS_POSITION_FROZEN
                || peep->PeepFlags & PEEP_FLAGS_ANIMATION_FROZEN)
                continue;

            // Release balloon
            GuestReleaseBalloon(peep, peep->z + 9);

            // Clap
            if ((peep->State == PeepState::walking || peep->State == PeepState::queuing) && peep->IsActionInterruptableSafely())
            {
                peep->Action = PeepActionType::clap;
                peep->AnimationFrameNum = 0;
                peep->AnimationImageIdOffset = 0;
                peep->UpdateCurrentAnimationType();
            }
        }

        // Play applause noise
        Play(SoundId::applause, 0, ContextGetWidth() / 2);
    }

    /**
     *
     *  rct2: 0x0069C35E
     */
    void PeepUpdateDaysInQueue()
    {
        for (auto peep : EntityList<Guest>())
        {
            if (!peep->outsideOfPark && (peep->State == PeepState::queuing))
            {
                peep->daysInQueue = AddClamp<uint8_t>(peep->daysInQueue, 1);
            }
        }
    }

    void Peep::FormatActionTo(Formatter& ft) const
    {
        switch (State)
        {
            case PeepState::falling:
                ft.Add<StringId>(Action == PeepActionType::drowning ? STR_DROWNING : STR_WALKING);
                break;
            case PeepState::one:
                ft.Add<StringId>(STR_WALKING);
                break;
            case PeepState::onRide:
            case PeepState::leavingRide:
            case PeepState::enteringRide:
            {
                auto ride = GetRide(CurrentRide);
                if (ride != nullptr)
                {
                    ft.Add<StringId>(
                        ride->getRideTypeDescriptor().flags.has(RtdFlag::describeAsInside) ? STR_IN_RIDE : STR_ON_RIDE);
                    ride->formatNameTo(ft);
                }
                else
                {
                    ft.Add<StringId>(STR_ON_RIDE).Add<StringId>(kStringIdNone);
                }
                break;
            }
            case PeepState::buying:
            {
                ft.Add<StringId>(STR_AT_RIDE);
                auto ride = GetRide(CurrentRide);
                if (ride != nullptr)
                {
                    ride->formatNameTo(ft);
                }
                else
                {
                    ft.Add<StringId>(kStringIdNone);
                }
                break;
            }
            case PeepState::walking:
            case PeepState::usingBin:
            {
                if (auto* guest = as<Guest>(); guest != nullptr)
                {
                    if (!guest->guestHeadingToRideId.IsNull())
                    {
                        auto ride = GetRide(guest->guestHeadingToRideId);
                        if (ride != nullptr)
                        {
                            ft.Add<StringId>(STR_HEADING_FOR);
                            ride->formatNameTo(ft);
                        }
                    }
                    else
                    {
                        ft.Add<StringId>((PeepFlags & PEEP_FLAGS_LEAVING_PARK) ? STR_LEAVING_PARK : STR_WALKING);
                    }
                }
                break;
            }
            case PeepState::queuingFront:
            case PeepState::queuing:
            {
                auto ride = GetRide(CurrentRide);
                if (ride != nullptr)
                {
                    ft.Add<StringId>(STR_QUEUING_FOR);
                    ride->formatNameTo(ft);
                }
                break;
            }
            case PeepState::sitting:
                ft.Add<StringId>(STR_SITTING);
                break;
            case PeepState::watching:
                if (!CurrentRide.IsNull())
                {
                    auto ride = GetRide(CurrentRide);
                    if (ride != nullptr)
                    {
                        ft.Add<StringId>((StandingFlags & 0x1) ? STR_WATCHING_CONSTRUCTION_OF : STR_WATCHING_RIDE);
                        ride->formatNameTo(ft);
                    }
                }
                else
                {
                    ft.Add<StringId>((StandingFlags & 0x1) ? STR_WATCHING_NEW_RIDE_BEING_CONSTRUCTED : STR_LOOKING_AT_SCENERY);
                }
                break;
            case PeepState::picked:
                ft.Add<StringId>(STR_SELECT_LOCATION);
                break;
            case PeepState::patrolling:
            case PeepState::enteringPark:
            case PeepState::leavingPark:
                ft.Add<StringId>(STR_WALKING);
                break;
            case PeepState::mowing:
                ft.Add<StringId>(STR_MOWING_GRASS);
                break;
            case PeepState::sweeping:
                ft.Add<StringId>(STR_SWEEPING_FOOTPATH);
                break;
            case PeepState::watering:
                ft.Add<StringId>(STR_WATERING_GARDENS);
                break;
            case PeepState::emptyingBin:
                ft.Add<StringId>(STR_EMPTYING_LITTER_BIN);
                break;
            case PeepState::answering:
                if (SubState == 0)
                {
                    ft.Add<StringId>(STR_WALKING);
                }
                else if (SubState == 1)
                {
                    ft.Add<StringId>(STR_ANSWERING_RADIO_CALL);
                }
                else
                {
                    ft.Add<StringId>(STR_RESPONDING_TO_RIDE_BREAKDOWN_CALL);
                    auto ride = GetRide(CurrentRide);
                    if (ride != nullptr)
                    {
                        ride->formatNameTo(ft);
                    }
                    else
                    {
                        ft.Add<StringId>(kStringIdNone);
                    }
                }
                break;
            case PeepState::fixing:
            {
                ft.Add<StringId>(STR_FIXING_RIDE);
                auto ride = GetRide(CurrentRide);
                if (ride != nullptr)
                {
                    ride->formatNameTo(ft);
                }
                else
                {
                    ft.Add<StringId>(kStringIdNone);
                }
                break;
            }
            case PeepState::headingToInspection:
            {
                ft.Add<StringId>(STR_HEADING_TO_RIDE_FOR_INSPECTION);
                auto ride = GetRide(CurrentRide);
                if (ride != nullptr)
                {
                    ride->formatNameTo(ft);
                }
                else
                {
                    ft.Add<StringId>(kStringIdNone);
                }
                break;
            }
            case PeepState::inspecting:
            {
                ft.Add<StringId>(STR_INSPECTING_RIDE);
                auto ride = GetRide(CurrentRide);
                if (ride != nullptr)
                {
                    ride->formatNameTo(ft);
                }
                else
                {
                    ft.Add<StringId>(kStringIdNone);
                }
                break;
            }
        }
    }

    static constexpr StringId kStaffNames[] = {
        STR_HANDYMAN_X,
        STR_MECHANIC_X,
        STR_SECURITY_GUARD_X,
        STR_ENTERTAINER_X,
    };

    void Peep::FormatNameTo(Formatter& ft) const
    {
        if (Name == nullptr)
        {
            auto& gameState = getGameState();
            const bool showGuestNames = gameState.park.flags & PARK_FLAGS_SHOW_REAL_GUEST_NAMES;
            const bool showStaffNames = gameState.park.flags & PARK_FLAGS_SHOW_REAL_STAFF_NAMES;

            auto* staff = as<Staff>();
            const bool isStaff = staff != nullptr;

            if ((!isStaff && showGuestNames) || (isStaff && showStaffNames))
            {
                auto nameId = PeepId;
                if (isStaff)
                {
                    // Prevent staff from getting the same names by offsetting the name table based on staff type.
                    nameId *= 256 * EnumValue(staff->assignedStaffType) + 1;
                }

                auto realNameStringId = GetRealNameStringIDFromPeepID(nameId);
                ft.Add<StringId>(realNameStringId);
            }
            else if (isStaff)
            {
                auto staffNameIndex = static_cast<uint8_t>(staff->assignedStaffType);
                if (staffNameIndex >= std::size(kStaffNames))
                {
                    staffNameIndex = 0;
                }

                ft.Add<StringId>(kStaffNames[staffNameIndex]);
                ft.Add<uint32_t>(PeepId);
            }
            else
            {
                ft.Add<StringId>(STR_GUEST_X).Add<uint32_t>(PeepId);
            }
        }
        else
        {
            ft.Add<StringId>(STR_STRING).Add<const char*>(Name);
        }
    }

    std::string Peep::GetName() const
    {
        Formatter ft;
        FormatNameTo(ft);
        return FormatStringIDLegacy(STR_STRINGID, ft.Data());
    }

    bool Peep::SetName(std::string_view value)
    {
        if (value.empty())
        {
            std::free(Name);
            Name = nullptr;
            return true;
        }

        auto newNameMemory = static_cast<char*>(std::malloc(value.size() + 1));
        if (newNameMemory != nullptr)
        {
            std::memcpy(newNameMemory, value.data(), value.size());
            newNameMemory[value.size()] = '\0';
            std::free(Name);
            Name = newNameMemory;
            return true;
        }
        return false;
    }

    bool Peep::IsActionWalking() const
    {
        return Action == PeepActionType::walking;
    }

    bool Peep::IsActionIdle() const
    {
        return Action == PeepActionType::idle;
    }

    bool Peep::IsActionInterruptable() const
    {
        return IsActionIdle() || IsActionWalking();
    }

    /**
     * Used to avoid peep action and animation triggers that cause them to stop moving and might put them at risk
     * of getting run over at level crossings, such as guests reading the map and entertainers performing.
     */
    bool Peep::IsActionInterruptableSafely() const
    {
        return IsActionInterruptable() && !IsOnLevelCrossing();
    }

    void PeepSetMapTooltip(Peep* peep)
    {
        auto ft = Formatter();
        if (auto* guest = peep->as<Guest>(); guest != nullptr)
        {
            ft.Add<StringId>((peep->PeepFlags & PEEP_FLAGS_TRACKING) ? STR_TRACKED_GUEST_MAP_TIP : STR_GUEST_MAP_TIP);
            ft.Add<uint32_t>(GetPeepFaceSpriteSmall(guest));
            guest->FormatNameTo(ft);
            guest->FormatActionTo(ft);
        }
        else
        {
            ft.Add<StringId>(STR_STAFF_MAP_TIP);
            peep->FormatNameTo(ft);
            peep->FormatActionTo(ft);
        }

        auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
        intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
        ContextBroadcastIntent(&intent);
    }

    /**
     *  rct2: 0x00693BAB
     */
    void Peep::SwitchNextAnimationType()
    {
        // TBD: Add nextAnimationType as function parameter and make peep->NextAnimationType obsolete?
        if (NextAnimationType != AnimationType)
        {
            invalidate();
            AnimationType = NextAnimationType;

            auto& objManager = GetContext()->GetObjectManager();
            auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);

            const auto& spriteBounds = animObj->GetSpriteBounds(AnimationGroup, NextAnimationType);
            spriteData.width = spriteBounds.spriteWidth;
            spriteData.heightMin = spriteBounds.spriteHeightNegative;
            spriteData.heightMax = spriteBounds.spriteHeightPositive;
            invalidate();
        }
    }

    /**
     *
     *  rct2: 0x00693EF2
     */
    static void PeepReturnToCentreOfTile(Peep* peep)
    {
        peep->PeepDirection = DirectionReverse(peep->PeepDirection);
        auto destination = peep->getLocation().ToTileCentre();
        peep->SetDestination(destination, 5);
    }

    /**
     *
     *  rct2: 0x00693f2C
     */
    static bool PeepInteractWithEntrance(Peep* peep, const CoordsXYE& coords, uint8_t& pathing_result)
    {
        auto tile_element = coords.element;
        uint8_t entranceType = tile_element->asEntrance()->GetEntranceType();
        auto rideIndex = tile_element->asEntrance()->GetRideIndex();

        if ((entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE) || (entranceType == ENTRANCE_TYPE_RIDE_EXIT))
        {
            // If an entrance or exit that doesn't belong to the ride we are queuing for ignore the entrance/exit
            // This can happen when paths clip through entrance/exits
            if (peep->State == PeepState::queuing && peep->CurrentRide != rideIndex)
            {
                return false;
            }
        }
        // Store some details to determine when to override the default
        // behaviour (defined below) for when staff attempt to enter a ride
        // to fix/inspect it.
        if (entranceType == ENTRANCE_TYPE_RIDE_EXIT)
        {
            pathing_result |= PATHING_RIDE_EXIT;
            _peepRideEntranceExitElement = tile_element;
        }
        else if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE)
        {
            pathing_result |= PATHING_RIDE_ENTRANCE;
            _peepRideEntranceExitElement = tile_element;
        }

        if (entranceType == ENTRANCE_TYPE_RIDE_EXIT)
        {
            // Default guest/staff behaviour attempting to enter a
            // ride exit is to turn around.
            peep->InteractionRideIndex = RideId::GetNull();
            PeepReturnToCentreOfTile(peep);
            return true;
        }

        if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE)
        {
            auto ride = GetRide(rideIndex);
            if (ride == nullptr)
                return false;

            auto* guest = peep->as<Guest>();
            if (guest == nullptr)
            {
                // Default staff behaviour attempting to enter a
                // ride entrance is to turn around.
                peep->InteractionRideIndex = RideId::GetNull();
                PeepReturnToCentreOfTile(peep);
                return true;
            }

            if (guest->State == PeepState::queuing)
            {
                // Guest is in the ride queue.
                guest->RideSubState = PeepRideSubState::atQueueFront;
                guest->AnimationImageIdOffset = _backupAnimationImageIdOffset;
                return true;
            }

            // Guest is on a normal path, i.e. ride has no queue.
            if (guest->InteractionRideIndex == rideIndex)
            {
                // Peep is retrying the ride entrance without leaving
                // the path tile and without trying any other ride
                // attached to this path tile. i.e. stick with the
                // peeps previous decision not to go on the ride.
                PeepReturnToCentreOfTile(guest);
                return true;
            }

            guest->timeLost = 0;
            auto stationNum = tile_element->asEntrance()->GetStationIndex();
            // Guest walks up to the ride for the first time since entering
            // the path tile or since considering another ride attached to
            // the path tile.
            if (!guest->shouldGoOnRide(*ride, stationNum, false, false))
            {
                // Peep remembers that this is the last ride they
                // considered while on this path tile.
                guest->InteractionRideIndex = rideIndex;
                PeepReturnToCentreOfTile(guest);
                return true;
            }

            // Guest has decided to go on the ride.
            guest->AnimationImageIdOffset = _backupAnimationImageIdOffset;
            guest->InteractionRideIndex = rideIndex;

            auto& station = ride->getStation(stationNum);
            auto previous_last = station.LastPeepInQueue;
            station.LastPeepInQueue = guest->id;
            guest->guestNextInQueue = previous_last;
            station.QueueLength++;

            guest->CurrentRide = rideIndex;
            guest->CurrentRideStation = stationNum;
            guest->daysInQueue = 0;
            guest->SetState(PeepState::queuing);
            guest->RideSubState = PeepRideSubState::atQueueFront;
            guest->timeInQueue = 0;
            if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
            {
                auto ft = Formatter();
                guest->FormatNameTo(ft);
                ride->formatNameTo(ft);
                if (Config::Get().notifications.guestQueuingForRide)
                {
                    News::AddItemToQueue(News::ItemType::peepOnRide, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, guest->id, ft);
                }
            }
        }
        else
        {
            // PARK_ENTRANCE
            auto* guest = peep->as<Guest>();
            if (guest == nullptr)
            {
                // Staff cannot leave the park, so go back.
                PeepReturnToCentreOfTile(peep);
                return true;
            }

            // If not the centre of the entrance arch
            if (tile_element->asEntrance()->GetSequenceIndex() != 0)
            {
                PeepReturnToCentreOfTile(guest);
                return true;
            }

            auto& gameState = getGameState();
            uint8_t entranceDirection = tile_element->getDirection();
            if (entranceDirection != guest->PeepDirection)
            {
                if (DirectionReverse(entranceDirection) != guest->PeepDirection)
                {
                    PeepReturnToCentreOfTile(guest);
                    return true;
                }

                // Peep is leaving the park.
                if (guest->State != PeepState::walking)
                {
                    PeepReturnToCentreOfTile(guest);
                    return true;
                }

                if (!(guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK))
                {
                    // If the park is open and leaving flag isn't set return to centre
                    if (gameState.park.flags & PARK_FLAGS_PARK_OPEN)
                    {
                        PeepReturnToCentreOfTile(guest);
                        return true;
                    }
                }

                auto destination = guest->GetDestination() + CoordsDirectionDelta[guest->PeepDirection];
                guest->SetDestination(destination, 9);
                guest->moveTo({ coords, guest->z });
                guest->SetState(PeepState::leavingPark);

                guest->Var37 = 0;
                if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
                {
                    auto ft = Formatter();
                    guest->FormatNameTo(ft);
                    if (Config::Get().notifications.guestLeftPark)
                    {
                        News::AddItemToQueue(News::ItemType::peepOnRide, STR_PEEP_TRACKING_LEFT_PARK, guest->id, ft);
                    }
                }
                return true;
            }

            // Peep is entering the park.

            if (guest->State != PeepState::enteringPark)
            {
                PeepReturnToCentreOfTile(guest);
                return true;
            }

            if (!(gameState.park.flags & PARK_FLAGS_PARK_OPEN))
            {
                guest->State = PeepState::leavingPark;
                guest->Var37 = 1;
                DecrementGuestsHeadingForPark();
                PeepWindowStateUpdate(guest);
                PeepReturnToCentreOfTile(guest);
                return true;
            }

            bool found = false;
            auto entrance = std::find_if(
                gameState.park.entrances.begin(), gameState.park.entrances.end(),
                [coords](const auto& e) { return coords.ToTileStart() == e; });
            if (entrance != gameState.park.entrances.end())
            {
                int16_t z = entrance->z / 8;
                entranceDirection = entrance->direction;
                auto nextLoc = coords.ToTileStart() + CoordsDirectionDelta[entranceDirection];

                // Make sure there is a path right behind the entrance, otherwise turn around
                TileElement* nextTileElement = MapGetFirstElementAt(nextLoc);
                do
                {
                    if (nextTileElement == nullptr)
                        break;
                    if (nextTileElement->getType() != TileElementType::Path)
                        continue;

                    if (nextTileElement->asPath()->IsQueue())
                        continue;

                    if (nextTileElement->asPath()->IsSloped())
                    {
                        uint8_t slopeDirection = nextTileElement->asPath()->GetSlopeDirection();
                        if (slopeDirection == entranceDirection)
                        {
                            if (z != nextTileElement->baseHeight)
                            {
                                continue;
                            }
                            found = true;
                            break;
                        }

                        if (DirectionReverse(slopeDirection) != entranceDirection)
                            continue;

                        if (z - 2 != nextTileElement->baseHeight)
                            continue;
                        found = true;
                        break;
                    }

                    if (z != nextTileElement->baseHeight)
                    {
                        continue;
                    }
                    found = true;
                    break;
                } while (!(nextTileElement++)->isLastForTile());
            }

            if (!found)
            {
                guest->State = PeepState::leavingPark;
                guest->Var37 = 1;
                DecrementGuestsHeadingForPark();
                PeepWindowStateUpdate(guest);
                PeepReturnToCentreOfTile(guest);
                return true;
            }

            auto entranceFee = Park::GetEntranceFee(gameState.park);
            if (entranceFee != 0)
            {
                if (guest->hasItem(ShopItem::voucher))
                {
                    if (guest->voucherType == VOUCHER_TYPE_PARK_ENTRY_HALF_PRICE)
                    {
                        entranceFee /= 2;
                        guest->removeItem(ShopItem::voucher);
                        guest->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
                    }
                    else if (guest->voucherType == VOUCHER_TYPE_PARK_ENTRY_FREE)
                    {
                        entranceFee = 0;
                        guest->removeItem(ShopItem::voucher);
                        guest->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
                    }
                }
                if (entranceFee > guest->cashInPocket)
                {
                    guest->State = PeepState::leavingPark;
                    guest->Var37 = 1;
                    DecrementGuestsHeadingForPark();
                    PeepWindowStateUpdate(guest);
                    PeepReturnToCentreOfTile(guest);
                    return true;
                }

                gameState.park.totalIncomeFromAdmissions = AddClamp(gameState.park.totalIncomeFromAdmissions, entranceFee);
                guest->spendMoney(guest->paidToEnter, entranceFee, ExpenditureType::parkEntranceTickets);
                guest->PeepFlags |= PEEP_FLAGS_HAS_PAID_FOR_PARK_ENTRY;
            }

            auto& park = getGameState().park;
            park.totalAdmissions = AddClamp<uint64_t>(park.totalAdmissions, 1);

            auto* windowMgr = Ui::GetWindowManager();
            windowMgr->InvalidateByNumber(WindowClass::parkInformation, 0);

            guest->Var37 = 1;
            auto destination = guest->GetDestination();
            destination += CoordsDirectionDelta[guest->PeepDirection];
            guest->SetDestination(destination, 7);
            guest->moveTo({ coords, guest->z });
        }
        return true;
    }

    /**
     *
     *  rct2: 0x006946D8
     */
    static void PeepFootpathMoveForward(Peep* peep, const CoordsXYE& coords, bool vandalism)
    {
        const auto* pathElement = coords.element->asPath();
        assert(pathElement != nullptr);

        peep->NextLoc = { coords.ToTileStart(), pathElement->getBaseZ() };
        peep->SetNextFlags(pathElement->GetSlopeDirection(), pathElement->IsSloped(), false);

        int16_t z = peep->GetZOnSlope(coords.x, coords.y);

        auto* guest = peep->as<Guest>();
        if (guest == nullptr)
        {
            peep->moveTo({ coords, z });
            return;
        }

        uint8_t vandalThoughtTimeout = (guest->vandalismSeen & 0xC0) >> 6;
        // Advance the vandalised tiles by 1
        uint8_t vandalisedTiles = (guest->vandalismSeen * 2) & 0x3F;

        if (vandalism)
        {
            // Add one more to the vandalised tiles
            vandalisedTiles |= 1;
            // If there has been 2 vandalised tiles in the last 6
            if (vandalisedTiles & 0x3E && (vandalThoughtTimeout == 0))
            {
                if ((ScenarioRand() & 0xFFFF) <= 10922)
                {
                    guest->insertNewThought(PeepThoughtType::vandalism);
                    guest->happinessTarget = std::max(0, guest->happinessTarget - 17);
                }
                vandalThoughtTimeout = 3;
            }
        }

        if (vandalThoughtTimeout && (ScenarioRand() & 0xFFFF) <= 4369)
        {
            vandalThoughtTimeout--;
        }

        guest->vandalismSeen = (vandalThoughtTimeout << 6) | vandalisedTiles;

        constexpr uint16_t kThresholdCrowdCount = 10;
        constexpr uint8_t kThresholdLitterCount = 3;
        constexpr uint8_t kThresholdVomitCount = 3;

        // Don't allow this loop to become too expensive, lets just have enough to potentially satisfy
        // all the conditions.
        constexpr uint32_t kMaxIterations = kThresholdCrowdCount + kThresholdLitterCount + kThresholdVomitCount;

        uint16_t crowdCount = 0;
        uint8_t litterCount = 0;
        uint8_t vomitCount = 0;
        uint32_t iterations = 0;

        auto quad = EntityTileList(coords);
        for (auto* otherEnt : quad)
        {
            if (iterations++ >= kMaxIterations)
            {
                break;
            }

            if (std::abs(otherEnt->z - guest->NextLoc.z) > 16)
                continue;

            if (const auto* otherPeep = otherEnt->as<Peep>(); otherPeep != nullptr)
            {
                if (otherPeep->State != PeepState::walking)
                    continue;

                crowdCount++;
                continue;
            }

            if (const auto* litter = otherEnt->as<Litter>(); litter != nullptr)
            {
                if (litter->subType != Litter::Type::vomit && litter->subType != Litter::Type::vomitAlt)
                {
                    litterCount++;
                }
                else
                {
                    vomitCount++;
                }
            }
        }

        if (crowdCount >= kThresholdCrowdCount && guest->State == PeepState::walking && (ScenarioRand() & 0xFFFF) <= 21845)
        {
            guest->insertNewThought(PeepThoughtType::crowded);
            guest->happinessTarget = std::max(0, guest->happinessTarget - 14);
        }

        litterCount = std::min(kThresholdLitterCount, litterCount);
        vomitCount = std::min(kThresholdVomitCount, vomitCount);

        uint8_t disgustingTime = guest->disgustingCount & 0xC0;
        uint8_t disgustingCount = ((guest->disgustingCount & 0xF) << 2) | vomitCount;
        guest->disgustingCount = disgustingCount | disgustingTime;

        if (disgustingTime & 0xC0 && (ScenarioRand() & 0xFFFF) <= 4369)
        {
            // Reduce the disgusting time
            guest->disgustingCount -= 0x40;
        }
        else
        {
            uint8_t totalDisgustingCount = 0;
            for (uint8_t time = 0; time < 3; time++)
            {
                totalDisgustingCount += (disgustingCount >> (2 * time)) & 0x3;
            }

            if (totalDisgustingCount >= kThresholdVomitCount && (ScenarioRand() & 0xFFFF) <= 10922)
            {
                guest->insertNewThought(PeepThoughtType::pathDisgusting);
                guest->happinessTarget = std::max(0, guest->happinessTarget - 17);
                // Reset disgusting time
                guest->disgustingCount |= 0xC0;
            }
        }

        uint8_t litterTime = guest->litterCount & 0xC0;
        litterCount = ((guest->litterCount & 0xF) << 2) | litterCount;
        guest->litterCount = litterCount | litterTime;

        if (litterTime & 0xC0 && (ScenarioRand() & 0xFFFF) <= 4369)
        {
            // Reduce the litter time
            guest->litterCount -= 0x40;
        }
        else
        {
            uint8_t totalLitter = 0;
            for (uint8_t time = 0; time < 3; time++)
            {
                totalLitter += (litterCount >> (2 * time)) & 0x3;
            }

            if (totalLitter >= kThresholdLitterCount && (ScenarioRand() & 0xFFFF) <= 10922)
            {
                guest->insertNewThought(PeepThoughtType::badLitter);
                guest->happinessTarget = std::max(0, guest->happinessTarget - 17);
                // Reset litter time
                guest->litterCount |= 0xC0;
            }
        }

        guest->moveTo({ coords, z });
    }

    /**
     *
     *  rct2: 0x0069455E
     */
    static void PeepInteractWithPath(Peep* peep, const CoordsXYE& coords)
    {
        // 0x00F1AEE2
        const auto* pathElement = coords.element->asPath();
        assert(pathElement != nullptr);

        bool vandalismPresent = false;
        if (pathElement->HasAddition() && pathElement->IsBroken() && (pathElement->GetEdges()) != 0xF)
        {
            vandalismPresent = true;
        }

        int16_t z = pathElement->getBaseZ();
        auto* guest = peep->as<Guest>();
        if (MapIsLocationOwned({ coords, z }))
        {
            if (guest != nullptr && guest->outsideOfPark)
            {
                PeepReturnToCentreOfTile(guest);
                return;
            }
        }
        else
        {
            if (guest == nullptr || !guest->outsideOfPark)
            {
                PeepReturnToCentreOfTile(peep);
                return;
            }
        }

        if (guest != nullptr && pathElement->IsQueue())
        {
            auto rideIndex = pathElement->GetRideIndex();
            if (guest->State == PeepState::queuing)
            {
                // Check if this queue is connected to the ride the
                // peep is queuing for, i.e. the player hasn't edited
                // the queue, rebuilt the ride, etc.
                if (guest->CurrentRide == rideIndex)
                {
                    PeepFootpathMoveForward(guest, coords, vandalismPresent);
                }
                else
                {
                    // Queue got disconnected from the original ride.
                    guest->InteractionRideIndex = RideId::GetNull();
                    guest->removeFromQueue();
                    guest->SetState(PeepState::one);
                    PeepFootpathMoveForward(guest, coords, vandalismPresent);
                }
            }
            else
            {
                // Peep is not queuing.
                guest->timeLost = 0;
                auto stationNum = pathElement->GetStationIndex();

                if (pathElement->HasQueueBanner()
                    && pathElement->GetQueueBannerDirection()
                        == DirectionReverse(guest->PeepDirection) // Ride sign is facing the direction the peep is walking
                )
                {
                    /* Peep is approaching the entrance of a ride queue.
                     * Decide whether to go on the ride. */
                    auto* ride = GetRide(rideIndex);
                    if (ride != nullptr && guest->shouldGoOnRide(*ride, stationNum, true, false))
                    {
                        // Peep has decided to go on the ride at the queue.
                        guest->InteractionRideIndex = rideIndex;

                        // Add the peep to the ride queue.
                        auto& station = ride->getStation(stationNum);
                        auto old_last_peep = station.LastPeepInQueue;
                        station.LastPeepInQueue = guest->id;
                        guest->guestNextInQueue = old_last_peep;
                        station.QueueLength++;

                        PeepDecrementNumRiders(guest);
                        guest->CurrentRide = rideIndex;
                        guest->CurrentRideStation = stationNum;
                        guest->State = PeepState::queuing;
                        guest->daysInQueue = 0;
                        PeepWindowStateUpdate(guest);

                        guest->RideSubState = PeepRideSubState::inQueue;
                        guest->DestinationTolerance = 2;
                        guest->timeInQueue = 0;
                        if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
                        {
                            auto ft = Formatter();
                            guest->FormatNameTo(ft);
                            ride->formatNameTo(ft);
                            if (Config::Get().notifications.guestQueuingForRide)
                            {
                                News::AddItemToQueue(
                                    News::ItemType::peepOnRide, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, guest->id, ft);
                            }
                        }

                        // Force set centre of tile to prevent issues with guests accidentally skipping the queue
                        auto queueTileCentre = CoordsXY{ CoordsXY{ guest->NextLoc }
                                                         + CoordsDirectionDelta[guest->PeepDirection] }
                                                   .ToTileCentre();
                        guest->SetDestination(queueTileCentre);

                        PeepFootpathMoveForward(guest, coords, vandalismPresent);
                    }
                    else
                    {
                        // Peep has decided not to go on the ride.
                        PeepReturnToCentreOfTile(guest);
                    }
                }
                else
                {
                    /* Peep is approaching a queue tile without a ride
                     * sign facing the peep. */
                    PeepFootpathMoveForward(guest, coords, vandalismPresent);
                }
            }
        }
        else
        {
            peep->InteractionRideIndex = RideId::GetNull();
            if (guest != nullptr && peep->State == PeepState::queuing)
            {
                guest->removeFromQueue();
                guest->SetState(PeepState::one);
            }
            PeepFootpathMoveForward(peep, coords, vandalismPresent);
        }
    }

    /**
     *
     *  rct2: 0x00693F70
     */
    static bool PeepInteractWithShop(Peep* peep, const CoordsXYE& coords)
    {
        RideId rideIndex = coords.element->asTrack()->GetRideIndex();
        auto ride = GetRide(rideIndex);
        if (ride == nullptr || !ride->getRideTypeDescriptor().flags.has(RtdFlag::isShopOrFacility))
            return false;

        auto* guest = peep->as<Guest>();
        if (guest == nullptr)
        {
            PeepReturnToCentreOfTile(peep);
            return true;
        }

        // If we are queuing ignore the 'shop'
        // This can happen when paths clip through track
        if (guest->State == PeepState::queuing)
        {
            return false;
        }

        guest->timeLost = 0;

        if (ride->status != RideStatus::open)
        {
            PeepReturnToCentreOfTile(guest);
            return true;
        }

        if (guest->InteractionRideIndex == rideIndex)
        {
            PeepReturnToCentreOfTile(guest);
            return true;
        }

        if (guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK)
        {
            PeepReturnToCentreOfTile(guest);
            return true;
        }

        if (ride->getRideTypeDescriptor().flags.has(RtdFlag::guestsShouldGoInsideFacility))
        {
            guest->timeLost = 0;
            if (!guest->shouldGoOnRide(*ride, StationIndex::FromUnderlying(0), false, false))
            {
                PeepReturnToCentreOfTile(guest);
                return true;
            }

            auto cost = ride->price[0];
            if (cost != 0 && !(getGameState().park.flags & PARK_FLAGS_NO_MONEY))
            {
                ride->totalProfit = AddClamp(ride->totalProfit, cost);
                ride->windowInvalidateFlags.set(RideInvalidateFlag::income);
                guest->spendMoney(cost, ExpenditureType::shopSales);
            }

            auto coordsCentre = coords.ToTileCentre();
            guest->SetDestination(coordsCentre, 3);
            guest->CurrentRide = rideIndex;
            guest->SetState(PeepState::enteringRide);
            guest->RideSubState = PeepRideSubState::approachShop;

            guest->guestTimeOnRide = 0;
            ride->curNumCustomers++;
            if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
            {
                auto ft = Formatter();
                guest->FormatNameTo(ft);
                ride->formatNameTo(ft);
                StringId string_id = ride->getRideTypeDescriptor().flags.has(RtdFlag::describeAsInside)
                    ? STR_PEEP_TRACKING_PEEP_IS_IN_X
                    : STR_PEEP_TRACKING_PEEP_IS_ON_X;
                if (Config::Get().notifications.guestUsedFacility)
                {
                    News::AddItemToQueue(News::ItemType::peepOnRide, string_id, guest->id, ft);
                }
            }
        }
        else
        {
            if (guest->guestHeadingToRideId == rideIndex)
                guest->guestHeadingToRideId = RideId::GetNull();
            guest->AnimationImageIdOffset = _backupAnimationImageIdOffset;
            guest->SetState(PeepState::buying);
            guest->CurrentRide = rideIndex;
            guest->SubState = 0;
        }

        return true;
    }

    /**
     *
     *  rct2: 0x00693C9E
     */
    std::pair<uint8_t, TileElement*> Peep::PerformNextAction()
    {
        uint8_t pathingResult = 0;
        TileElement* tileResult = nullptr;

        PeepActionType previousAction = Action;

        if (Action == PeepActionType::idle)
            Action = PeepActionType::walking;

        auto* guest = as<Guest>();
        if (State == PeepState::queuing && guest != nullptr)
        {
            if (guest->updateQueuePosition(previousAction))
            {
                return { pathingResult, tileResult };
            }
        }

        std::optional<CoordsXY> loc;
        if (loc = UpdateAction(); !loc.has_value())
        {
            pathingResult |= PATHING_DESTINATION_REACHED;
            uint8_t result = 0;

            if (guest != nullptr)
            {
                result = PathFinding::CalculateNextDestination(*guest);
            }
            else
            {
                auto* staff = as<Staff>();
                result = staff->doPathFinding();
            }

            if (result != 0)
                return { pathingResult, tileResult };

            if (loc = UpdateAction(); !loc.has_value())
                return { pathingResult, tileResult };
        }

        auto newLoc = *loc;
        CoordsXY truncatedNewLoc = newLoc.ToTileStart();
        if (truncatedNewLoc == CoordsXY{ NextLoc })
        {
            int16_t height = GetZOnSlope(newLoc.x, newLoc.y);
            moveTo({ newLoc.x, newLoc.y, height });
            return { pathingResult, tileResult };
        }

        if (MapIsEdge(newLoc))
        {
            if (guest != nullptr && guest->outsideOfPark)
            {
                pathingResult |= PATHING_OUTSIDE_PARK;
            }
            PeepReturnToCentreOfTile(this);
            return { pathingResult, tileResult };
        }

        TileElement* tileElement = MapGetFirstElementAt(newLoc);
        if (tileElement == nullptr)
            return { pathingResult, tileResult };

        int16_t base_z = std::max(0, (z / 8) - 2);
        int16_t top_z = (z / 8) + 1;

        do
        {
            if (base_z > tileElement->baseHeight)
                continue;
            if (top_z < tileElement->baseHeight)
                continue;
            if (tileElement->isGhost())
                continue;

            if (tileElement->getType() == TileElementType::Path)
            {
                PeepInteractWithPath(this, { newLoc, tileElement });
                tileResult = tileElement;

                return { pathingResult, tileResult };
            }

            if (tileElement->getType() == TileElementType::Track)
            {
                if (PeepInteractWithShop(this, { newLoc, tileElement }))
                {
                    tileResult = tileElement;
                    return { pathingResult, tileResult };
                }
            }
            else if (tileElement->getType() == TileElementType::Entrance)
            {
                if (PeepInteractWithEntrance(this, { newLoc, tileElement }, pathingResult))
                {
                    tileResult = tileElement;
                    return { pathingResult, tileResult };
                }
            }
        } while (!(tileElement++)->isLastForTile());

        if (is<Staff>() || (GetNextIsSurface()))
        {
            int16_t height = abs(TileElementHeight(newLoc) - z);
            if (height <= 3 || (is<Staff>() && height <= 32))
            {
                InteractionRideIndex = RideId::GetNull();
                if (guest != nullptr && State == PeepState::queuing)
                {
                    guest->removeFromQueue();
                    SetState(PeepState::one);
                }

                if (!MapIsLocationInPark(newLoc))
                {
                    PeepReturnToCentreOfTile(this);
                    return { pathingResult, tileResult };
                }

                auto surfaceElement = MapGetSurfaceElementAt(newLoc);
                if (surfaceElement == nullptr)
                {
                    PeepReturnToCentreOfTile(this);
                    return { pathingResult, tileResult };
                }

                int16_t water_height = surfaceElement->GetWaterHeight();
                if (water_height > 0)
                {
                    PeepReturnToCentreOfTile(this);
                    return { pathingResult, tileResult };
                }

                auto* staff = as<Staff>();
                if (staff != nullptr && !GetNextIsSurface())
                {
                    // Prevent staff from leaving the path on their own unless they're allowed to mow.
                    if (!((staff->staffOrders & STAFF_ORDERS_MOWING) && staff->staffMowingTimeout >= 12))
                    {
                        PeepReturnToCentreOfTile(staff);
                        return { pathingResult, tileResult };
                    }
                }

                // The peep is on a surface and not on a path
                NextLoc = { truncatedNewLoc, surfaceElement->getBaseZ() };
                SetNextFlags(0, false, true);

                height = GetZOnSlope(newLoc.x, newLoc.y);
                moveTo({ newLoc.x, newLoc.y, height });
                return { pathingResult, tileResult };
            }
        }

        PeepReturnToCentreOfTile(this);
        return { pathingResult, tileResult };
    }

    /**
     * Gets the height including the bit depending on how far up the slope the peep
     * is.
     *  rct2: 0x00694921
     */
    int32_t Peep::GetZOnSlope(int32_t tile_x, int32_t tile_y)
    {
        if (tile_x == kLocationNull)
            return 0;

        if (GetNextIsSurface())
        {
            return TileElementHeight({ tile_x, tile_y });
        }

        uint8_t slope = GetNextDirection();
        return NextLoc.z + MapHeightFromSlope({ tile_x, tile_y }, slope, GetNextIsSloped());
    }

    StringId GetRealNameStringIDFromPeepID(uint32_t id)
    {
        // Generate a name_string_idx from the peep id using bit twiddling
        uint16_t ax = static_cast<uint16_t>(id + 0xF0B);
        uint16_t dx = 0;
        static constexpr uint16_t twiddlingBitOrder[] = {
            4, 9, 3, 7, 5, 8, 2, 1, 6, 0, 12, 11, 13, 10,
        };
        for (size_t i = 0; i < std::size(twiddlingBitOrder); i++)
        {
            dx |= (ax & (1 << twiddlingBitOrder[i]) ? 1 : 0) << i;
        }
        ax = dx & 0xF;
        dx *= 4;
        ax *= 4096;
        dx += ax;
        if (dx < ax)
        {
            dx += 0x1000;
        }
        dx /= 4;
        dx += kRealNameStart;
        return dx;
    }

    int32_t PeepCompare(const EntityId sprite_index_a, const EntityId sprite_index_b)
    {
        Peep const* peep_a = getGameState().entities.GetEntity<Peep>(sprite_index_a);
        Peep const* peep_b = getGameState().entities.GetEntity<Peep>(sprite_index_b);
        if (peep_a == nullptr || peep_b == nullptr)
        {
            return 0;
        }

        // Compare types
        if (peep_a->type != peep_b->type)
        {
            return static_cast<int32_t>(peep_a->type) - static_cast<int32_t>(peep_b->type);
        }

        if (peep_a->Name == nullptr && peep_b->Name == nullptr)
        {
            if (getGameState().park.flags & PARK_FLAGS_SHOW_REAL_GUEST_NAMES)
            {
                // Potentially could find a more optional way of sorting dynamic real names
            }
            else
            {
                // Simple ID comparison for when both peeps use a number or a generated name
                return peep_a->PeepId - peep_b->PeepId;
            }
        }

        // Compare their names as strings
        char nameA[256]{};
        Formatter ft;
        peep_a->FormatNameTo(ft);
        FormatStringLegacy(nameA, sizeof(nameA), STR_STRINGID, ft.Data());

        char nameB[256]{};
        ft.Rewind();
        peep_b->FormatNameTo(ft);
        FormatStringLegacy(nameB, sizeof(nameB), STR_STRINGID, ft.Data());
        return String::logicalCmp(nameA, nameB);
    }

    /**
     *
     *  rct2: 0x0069926C
     */
    void PeepUpdateNames()
    {
        auto& gameState = getGameState();
        auto& config = Config::Get().general;

        if (config.showRealNamesOfGuests)
            gameState.park.flags |= PARK_FLAGS_SHOW_REAL_GUEST_NAMES;
        else
            gameState.park.flags &= ~PARK_FLAGS_SHOW_REAL_GUEST_NAMES;

        if (config.showRealNamesOfStaff)
            gameState.park.flags |= PARK_FLAGS_SHOW_REAL_STAFF_NAMES;
        else
            gameState.park.flags &= ~PARK_FLAGS_SHOW_REAL_STAFF_NAMES;

        auto intent = Intent(INTENT_ACTION_REFRESH_GUEST_LIST);
        ContextBroadcastIntent(&intent);
        GfxInvalidateScreen();
    }

    void IncrementGuestsInPark()
    {
        auto& gameState = getGameState();
        if (gameState.park.numGuestsInPark < UINT32_MAX)
        {
            gameState.park.numGuestsInPark++;
        }
        else
        {
            Guard::Fail("Attempt to increment guests in park above max value (65535).");
        }
    }

    void IncrementGuestsHeadingForPark()
    {
        auto& gameState = getGameState();
        if (gameState.park.numGuestsHeadingForPark < UINT32_MAX)
        {
            gameState.park.numGuestsHeadingForPark++;
        }
        else
        {
            Guard::Fail("Attempt to increment guests heading for park above max value (65535).");
        }
    }

    void DecrementGuestsInPark()
    {
        auto& gameState = getGameState();
        if (gameState.park.numGuestsInPark > 0)
        {
            gameState.park.numGuestsInPark--;
        }
        else
        {
            LOG_ERROR("Attempt to decrement guests in park below zero.");
        }
    }

    void DecrementGuestsHeadingForPark()
    {
        auto& gameState = getGameState();

        if (gameState.park.numGuestsHeadingForPark > 0)
        {
            gameState.park.numGuestsHeadingForPark--;
        }
        else
        {
            LOG_ERROR("Attempt to decrement guests heading for park below zero.");
        }
    }

    static void GuestReleaseBalloon(Guest* peep, int16_t spawn_height)
    {
        if (peep->hasItem(ShopItem::balloon))
        {
            peep->removeItem(ShopItem::balloon);

            if (peep->AnimationGroup == PeepAnimationGroup::balloon && peep->x != kLocationNull)
            {
                Balloon::Create({ peep->x, peep->y, spawn_height }, peep->balloonColour, false);
                peep->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
                peep->updateAnimationGroup();
            }
        }
    }

    /**
     *
     *  rct2: 0x0069A512
     */
    void Peep::RemoveFromRide()
    {
        auto* guest = as<Guest>();
        if (guest != nullptr && State == PeepState::queuing)
        {
            guest->removeFromQueue();
        }
        StateReset();
    }

    void Peep::SetDestination(const CoordsXY& coords)
    {
        DestinationX = static_cast<uint16_t>(coords.x);
        DestinationY = static_cast<uint16_t>(coords.y);
    }

    void Peep::SetDestination(const CoordsXY& coords, int32_t tolerance)
    {
        SetDestination(coords);
        DestinationTolerance = tolerance;
    }

    CoordsXY Peep::GetDestination() const
    {
        return CoordsXY{ DestinationX, DestinationY };
    }

    void Peep::serialise(DataSerialiser& stream)
    {
        EntityBase::serialise(stream);
        if (stream.IsLoading())
        {
            Name = nullptr;
        }
        stream << NextLoc;
        stream << NextFlags;
        stream << State;
        stream << SubState;
        stream << AnimationGroup;
        stream << TshirtColour;
        stream << TrousersColour;
        stream << DestinationX;
        stream << DestinationY;
        stream << DestinationTolerance;
        stream << Var37;
        stream << Energy;
        stream << EnergyTarget;
        stream << Mass;
        // stream << base.WindowInvalidateFlags;
        stream << CurrentRide;
        stream << CurrentRideStation;
        stream << CurrentTrain;
        stream << CurrentCar;
        stream << CurrentSeat;
        stream << SpecialSprite;
        stream << AnimationType;
        stream << NextAnimationType;
        stream << AnimationImageIdOffset;
        stream << Action;
        stream << AnimationFrameNum;
        stream << StepProgress;
        stream << PeepDirection;
        stream << InteractionRideIndex;
        stream << PeepId;
        stream << PathCheckOptimisation;
        stream << PathfindGoal;
        stream << PathfindHistory;
        stream << WalkingAnimationFrameNum;
        stream << PeepFlags;
    }

    /**
     *
     *  rct2: 0x0069A98C
     */
    void Peep::ResetPathfindGoal()
    {
        PathfindGoal.SetNull();
        PathfindGoal.direction = kInvalidDirection;
    }
} // namespace OpenRCT2
