/*****************************************************************************
 * 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 "../Context.h"
#include "../Diagnostic.h"
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../audio/Audio.h"
#include "../audio/AudioChannel.h"
#include "../audio/AudioMixer.h"
#include "../config/Config.h"
#include "../core/DataSerialiser.h"
#include "../core/EnumUtils.hpp"
#include "../core/Guard.hpp"
#include "../core/String.hpp"
#include "../drawing/Drawing.h"
#include "../drawing/PickupPeep.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 "../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 "../scenario/Scenario.h"
#include "../ui/WindowManager.h"
#include "../util/Util.h"
#include "../windows/Intent.h"
#include "../world/ConstructionClearance.h"
#include "../world/Footpath.h"
#include "../world/Map.h"
#include "../world/Park.h"
#include "../world/QuarterTile.h"
#include "../world/tile_element/EntranceElement.h"
#include "../world/tile_element/PathElement.h"
#include "../world/tile_element/SurfaceElement.h"
#include "../world/tile_element/TileElement.h"
#include "../world/tile_element/TrackElement.h"
#include "PatrolArea.h"
#include "Staff.h"

#include <cassert>
#include <iterator>
#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;
        }

        pickupPeepClear();
    }

    // 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.has(PeepFlag::slowWalk) && 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.has(PeepFlag::positionFrozen)
                || peep->peepFlags.has(PeepFlag::animationFrozen))
                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.has(PeepFlag::leavingPark) ? 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.has(ParkFlag::showRealGuestNames);
            const bool showStaffNames = gameState.park.flags.has(ParkFlag::showRealStaffNames);

            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.has(PeepFlag::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;
        auto entranceType = tile_element->asEntrance()->getEntranceType();
        auto rideIndex = tile_element->asEntrance()->getRideIndex();

        if ((entranceType == EntranceType::rideEntrance) || (entranceType == EntranceType::rideExit))
        {
            // 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 == EntranceType::rideExit)
        {
            pathing_result |= PATHING_RIDE_EXIT;
            _peepRideEntranceExitElement = tile_element;
        }
        else if (entranceType == EntranceType::rideEntrance)
        {
            pathing_result |= PATHING_RIDE_ENTRANCE;
            _peepRideEntranceExitElement = tile_element;
        }

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

        if (entranceType == EntranceType::rideEntrance)
        {
            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.has(PeepFlag::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 (tile_element->asEntrance()->getSequenceIndex() != ParkEntranceSequence::centre)
            {
                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.has(PeepFlag::leavingPark))
                {
                    // If the park is open and leaving flag isn't set return to centre
                    if (gameState.park.flags.has(ParkFlag::parkOpen))
                    {
                        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.has(PeepFlag::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.has(ParkFlag::parkOpen))
            {
                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.set(PeepFlag::hasPaidForParkEntry);
            }

            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.has(PeepFlag::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.has(PeepFlag::leavingPark))
        {
            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.has(ParkFlag::noMoney))
            {
                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.has(PeepFlag::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.has(ParkFlag::showRealGuestNames))
            {
                // 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;

        gameState.park.flags.set(ParkFlag::showRealGuestNames, config.showRealNamesOfGuests);
        gameState.park.flags.set(ParkFlag::showRealStaffNames, config.showRealNamesOfStaff);

        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.holder;
    }

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