/***************************************************************************** * Copyright (c) 2014-2020 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "Peep.h" #include "../Cheats.h" #include "../Context.h" #include "../Game.h" #include "../Input.h" #include "../OpenRCT2.h" #include "../audio/AudioMixer.h" #include "../audio/audio.h" #include "../config/Config.h" #include "../core/Guard.hpp" #include "../interface/Window.h" #include "../localisation/Localisation.h" #include "../management/Finance.h" #include "../management/Marketing.h" #include "../management/NewsItem.h" #include "../network/network.h" #include "../ride/Ride.h" #include "../ride/RideData.h" #include "../ride/ShopItem.h" #include "../ride/Station.h" #include "../ride/Track.h" #include "../scenario/Scenario.h" #include "../sprites.h" #include "../util/Util.h" #include "../windows/Intent.h" #include "../world/Climate.h" #include "../world/Entrance.h" #include "../world/Footpath.h" #include "../world/LargeScenery.h" #include "../world/Map.h" #include "../world/Park.h" #include "../world/Scenery.h" #include "../world/SmallScenery.h" #include "../world/Sprite.h" #include "../world/Surface.h" #include "Staff.h" #include #include #include #if defined(DEBUG_LEVEL_1) && DEBUG_LEVEL_1 bool gPathFindDebug = false; utf8 gPathFindDebugPeepName[256]; #endif // defined(DEBUG_LEVEL_1) && DEBUG_LEVEL_1 uint8_t gGuestChangeModifier; uint32_t gNumGuestsInPark; uint32_t gNumGuestsInParkLastWeek; uint32_t gNumGuestsHeadingForPark; money16 gGuestInitialCash; uint8_t gGuestInitialHappiness; uint8_t gGuestInitialHunger; uint8_t gGuestInitialThirst; uint32_t gNextGuestNumber; uint8_t gPeepWarningThrottle[16]; TileCoordsXYZ gPeepPathFindGoalPosition; bool gPeepPathFindIgnoreForeignQueues; ride_id_t gPeepPathFindQueueRideIndex; static uint8_t _unk_F1AEF0; static TileElement* _peepRideEntranceExitElement; static void* _crowdSoundChannel = nullptr; static void peep_128_tick_update(Peep* peep, int32_t index); static void peep_release_balloon(Guest* peep, int16_t spawn_height); // clang-format off // Flags used by PeepThoughtToActionMap enum PeepThoughtToActionFlag : uint8_t { PEEP_THOUGHT_ACTION_NO_FLAGS = 0, PEEP_THOUGHT_ACTION_FLAG_RIDE = (1 << 0), PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_SINGULAR = (1 << 1), PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE = (1 << 2), }; /** rct2: 0x00981DB0 */ static struct { PeepActionType action; PeepThoughtToActionFlag flags; } PeepThoughtToActionMap[] = { { PEEP_ACTION_SHAKE_HEAD, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_EMPTY_POCKETS, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_WOW, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_SINGULAR }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE }, { PEEP_ACTION_SHAKE_HEAD, PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_WAVE, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_JOY, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_CHECK_TIME, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_WAVE, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_WAVE, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_DISGUST, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_BEING_WATCHED, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_SHAKE_HEAD, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_JOY, PEEP_THOUGHT_ACTION_NO_FLAGS }, { PEEP_ACTION_NONE_2, PEEP_THOUGHT_ACTION_FLAG_RIDE }, }; static PeepActionSpriteType PeepSpecialSpriteToSpriteTypeMap[] = { PEEP_ACTION_SPRITE_TYPE_NONE, PEEP_ACTION_SPRITE_TYPE_HOLD_MAT, PEEP_ACTION_SPRITE_TYPE_STAFF_MOWER }; static PeepActionSpriteType PeepActionToSpriteTypeMap[] = { PEEP_ACTION_SPRITE_TYPE_CHECK_TIME, PEEP_ACTION_SPRITE_TYPE_EAT_FOOD, PEEP_ACTION_SPRITE_TYPE_SHAKE_HEAD, PEEP_ACTION_SPRITE_TYPE_EMPTY_POCKETS, PEEP_ACTION_SPRITE_TYPE_SITTING_EAT_FOOD, PEEP_ACTION_SPRITE_TYPE_SITTING_LOOK_AROUND_LEFT, PEEP_ACTION_SPRITE_TYPE_SITTING_LOOK_AROUND_RIGHT, PEEP_ACTION_SPRITE_TYPE_WOW, PEEP_ACTION_SPRITE_TYPE_THROW_UP, PEEP_ACTION_SPRITE_TYPE_JUMP, PEEP_ACTION_SPRITE_TYPE_STAFF_SWEEP, PEEP_ACTION_SPRITE_TYPE_DROWNING, PEEP_ACTION_SPRITE_TYPE_STAFF_ANSWER_CALL, PEEP_ACTION_SPRITE_TYPE_STAFF_ANSWER_CALL_2, PEEP_ACTION_SPRITE_TYPE_STAFF_CHECKBOARD, PEEP_ACTION_SPRITE_TYPE_STAFF_FIX, PEEP_ACTION_SPRITE_TYPE_STAFF_FIX_2, PEEP_ACTION_SPRITE_TYPE_STAFF_FIX_GROUND, PEEP_ACTION_SPRITE_TYPE_STAFF_FIX_3, PEEP_ACTION_SPRITE_TYPE_STAFF_WATERING, PEEP_ACTION_SPRITE_TYPE_JOY, PEEP_ACTION_SPRITE_TYPE_READ_MAP, PEEP_ACTION_SPRITE_TYPE_WAVE, PEEP_ACTION_SPRITE_TYPE_STAFF_EMPTY_BIN, PEEP_ACTION_SPRITE_TYPE_WAVE_2, PEEP_ACTION_SPRITE_TYPE_TAKE_PHOTO, PEEP_ACTION_SPRITE_TYPE_CLAP, PEEP_ACTION_SPRITE_TYPE_DISGUST, PEEP_ACTION_SPRITE_TYPE_DRAW_PICTURE, PEEP_ACTION_SPRITE_TYPE_BEING_WATCHED, PEEP_ACTION_SPRITE_TYPE_WITHDRAW_MONEY }; const bool gSpriteTypeToSlowWalkMap[] = { false, false, false, false, false, false, false, false, false, false, false, true, false, false, true, true, true, true, true, false, true, false, true, true, true, false, false, true, true, false, false, true, true, true, true, true, true, true, false, true, false, true, true, true, true, true, true, true, }; // clang-format on bool rct_sprite::IsPeep() const { return peep.sprite_identifier == SPRITE_IDENTIFIER_PEEP; } Peep* rct_sprite::AsPeep() { Peep* result = nullptr; if (IsPeep()) { return reinterpret_cast(this); } return result; } Guest* Peep::AsGuest() { return type == PEEP_TYPE_GUEST ? static_cast(this) : nullptr; } Staff* Peep::AsStaff() { return type == PEEP_TYPE_STAFF ? static_cast(this) : nullptr; } void Peep::Invalidate() { invalidate_sprite_2(this); } void Peep::MoveTo(const CoordsXYZ& newLocation) { Invalidate(); // Invalidate current position. SpriteBase::MoveTo(newLocation); Invalidate(); // Invalidate new position. } uint8_t Peep::GetNextDirection() const { return next_flags & PEEP_NEXT_FLAG_DIRECTION_MASK; } bool Peep::GetNextIsSloped() const { return next_flags & PEEP_NEXT_FLAG_IS_SLOPED; } bool Peep::GetNextIsSurface() const { return next_flags & PEEP_NEXT_FLAG_IS_SURFACE; } void Peep::SetNextFlags(uint8_t next_direction, bool is_sloped, bool is_surface) { next_flags = next_direction & PEEP_NEXT_FLAG_DIRECTION_MASK; next_flags |= is_sloped ? PEEP_NEXT_FLAG_IS_SLOPED : 0; next_flags |= is_surface ? PEEP_NEXT_FLAG_IS_SURFACE : 0; } Peep* try_get_guest(uint16_t spriteIndex) { rct_sprite* sprite = try_get_sprite(spriteIndex); if (sprite == nullptr) return nullptr; if (!sprite->IsPeep()) return nullptr; if (sprite->peep.type != PEEP_TYPE_GUEST) return nullptr; return &sprite->peep; } int32_t peep_get_staff_count() { uint16_t spriteIndex; Peep* peep; int32_t count = 0; FOR_ALL_STAFF (spriteIndex, peep) count++; return count; } /** * * rct2: 0x0068F0A9 */ void peep_update_all() { int32_t i = 0; uint16_t spriteIndex; Peep* peep; if (gScreenFlags & SCREEN_FLAGS_EDITOR) return; // Do not use the FOR_ALL_PEEPS macro for this as next sprite index // will be fetched on a delted peep if peep leaves the park. for (spriteIndex = gSpriteListHead[SPRITE_LIST_PEEP]; spriteIndex != SPRITE_INDEX_NULL;) { peep = GET_PEEP(spriteIndex); spriteIndex = peep->next; if (static_cast(i & 0x7F) != (gCurrentTicks & 0x7F)) { peep->Update(); } else { peep_128_tick_update(peep, i); if (peep->sprite_identifier == SPRITE_IDENTIFIER_PEEP) { peep->Update(); } } i++; } } /** * * rct2: 0x0068F41A * Called every 128 ticks */ static void peep_128_tick_update(Peep* peep, int32_t index) { auto guest = peep->AsGuest(); if (guest != nullptr) { guest->Tick128UpdateGuest(index); } else { auto staff = peep->AsStaff(); if (staff != nullptr) { staff->Tick128UpdateStaff(); } } } /* * 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() { path_check_optimisation++; if ((path_check_optimisation & 0xF) != (sprite_index & 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 = map_get_first_element_at(NextLoc); uint8_t map_type = TILE_ELEMENT_TYPE_PATH; if (GetNextIsSurface()) { map_type = TILE_ELEMENT_TYPE_SURFACE; } do { if (tile_element == nullptr) break; if (tile_element->GetType() == map_type) { if (NextLoc.z == tile_element->GetBaseZ()) { // Found a suitable path or surface return true; } } } while (!(tile_element++)->IsLastForTile()); // Found no suitable path SetState(PEEP_STATE_FALLING); return false; } PeepActionSpriteType Peep::GetActionSpriteType() { if (action >= PEEP_ACTION_NONE_1) { // PEEP_ACTION_NONE_1 or PEEP_ACTION_NONE_2 return PeepSpecialSpriteToSpriteTypeMap[special_sprite]; } else if (action < std::size(PeepActionToSpriteTypeMap)) { return PeepActionToSpriteTypeMap[action]; } else { openrct2_assert( action >= std::size(PeepActionToSpriteTypeMap) && action < PEEP_ACTION_NONE_1, "Invalid peep action %u", action); return PEEP_ACTION_SPRITE_TYPE_NONE; } } /* * rct2: 0x00693B58 */ void Peep::UpdateCurrentActionSpriteType() { if (sprite_type >= std::size(g_peep_animation_entries)) { return; } PeepActionSpriteType newActionSpriteType = GetActionSpriteType(); if (action_sprite_type == newActionSpriteType) { return; } Invalidate(); action_sprite_type = newActionSpriteType; const rct_sprite_bounds* spriteBounds = g_peep_animation_entries[sprite_type].sprite_bounds; sprite_width = spriteBounds[action_sprite_type].sprite_width; sprite_height_negative = spriteBounds[action_sprite_type].sprite_height_negative; sprite_height_positive = spriteBounds[action_sprite_type].sprite_height_positive; Invalidate(); } /* rct2: 0x00693BE5 */ void Peep::SwitchToSpecialSprite(uint8_t special_sprite_id) { if (special_sprite_id == special_sprite) return; special_sprite = special_sprite_id; // If NONE_1 or NONE_2 if (action >= PEEP_ACTION_NONE_1) { action_sprite_image_offset = 0; } UpdateCurrentActionSpriteType(); } void Peep::StateReset() { SetState(PEEP_STATE_1); SwitchToSpecialSprite(0); } /** rct2: 0x00981D7C, 0x00981D7E */ static constexpr const CoordsXY word_981D7C[4] = { { -2, 0 }, { 0, 2 }, { 2, 0 }, { 0, -2 } }; std::optional 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 Peep::UpdateAction(int16_t& xy_distance) { _unk_F1AEF0 = action_sprite_image_offset; if (action == PEEP_ACTION_NONE_1) { action = PEEP_ACTION_NONE_2; } CoordsXY diffrenceLoc = { x - destination_x, y - destination_y }; int32_t x_delta = abs(diffrenceLoc.x); int32_t y_delta = abs(diffrenceLoc.y); xy_distance = x_delta + y_delta; if (action == PEEP_ACTION_NONE_1 || action == PEEP_ACTION_NONE_2) { if (xy_distance <= destination_tolerance) { return std::nullopt; } int32_t nextDirection = 0; if (x_delta < y_delta) { nextDirection = 8; if (diffrenceLoc.y >= 0) { nextDirection = 24; } } else { nextDirection = 16; if (diffrenceLoc.x >= 0) { nextDirection = 0; } } sprite_direction = nextDirection; CoordsXY loc = { x, y }; loc += word_981D7C[nextDirection / 8]; no_action_frame_num++; const rct_peep_animation* peepAnimation = g_peep_animation_entries[sprite_type].sprite_animation; const uint8_t* imageOffset = peepAnimation[action_sprite_type].frame_offsets; if (no_action_frame_num >= peepAnimation[action_sprite_type].num_frames) { no_action_frame_num = 0; } action_sprite_image_offset = imageOffset[no_action_frame_num]; return loc; } const rct_peep_animation* peepAnimation = g_peep_animation_entries[sprite_type].sprite_animation; action_frame++; // If last frame of action if (action_frame >= peepAnimation[action_sprite_type].num_frames) { action_sprite_image_offset = 0; action = PEEP_ACTION_NONE_2; UpdateCurrentActionSpriteType(); return { { x, y } }; } action_sprite_image_offset = peepAnimation[action_sprite_type].frame_offsets[action_frame]; // If not throwing up and not at the frame where sick appears. if (action != PEEP_ACTION_THROW_UP || action_frame != 15) { return { { x, y } }; } // We are throwing up hunger /= 2; nausea_target /= 2; if (nausea < 30) nausea = 0; else nausea -= 30; window_invalidate_flags |= PEEP_INVALIDATE_PEEP_2; // Create sick at location litter_create(x, y, z, sprite_direction, (sprite_index & 1) ? LITTER_TYPE_SICK_ALT : LITTER_TYPE_SICK); SoundId coughs[4] = { SoundId::Cough1, SoundId::Cough2, SoundId::Cough3, SoundId::Cough4 }; auto soundId = coughs[scenario_rand() & 3]; audio_play_sound_at_location(soundId, { x, y, z }); return { { x, y } }; } /** * rct2: 0x0069A409 * Decreases rider count if on/entering a ride. */ void peep_decrement_num_riders(Peep* peep) { if (peep->state == PEEP_STATE_ON_RIDE || peep->state == PEEP_STATE_ENTERING_RIDE) { auto ride = get_ride(peep->current_ride); if (ride != nullptr) { ride->num_riders = std::max(0, ride->num_riders - 1); ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_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 peep_window_state_update(Peep* peep) { rct_window* w = window_find_by_number(WC_PEEP, peep->sprite_index); if (w != nullptr) window_event_invalidate_call(w); if (peep->type == PEEP_TYPE_GUEST) { if (peep->state == PEEP_STATE_ON_RIDE || peep->state == PEEP_STATE_ENTERING_RIDE) { auto ride = get_ride(peep->current_ride); if (ride != nullptr) { ride->num_riders++; ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST; } } window_invalidate_by_number(WC_PEEP, peep->sprite_index); window_invalidate_by_class(WC_GUEST_LIST); } else { window_invalidate_by_number(WC_PEEP, peep->sprite_index); window_invalidate_by_class(WC_STAFF_LIST); } } void Peep::Pickup() { auto guest = AsGuest(); if (guest != nullptr) { guest->RemoveFromRide(); } MoveTo({ LOCATION_NULL, y, z }); SetState(PEEP_STATE_PICKED); sub_state = 0; } void Peep::PickupAbort(int32_t old_x) { if (state != PEEP_STATE_PICKED) return; MoveTo({ old_x, y, z + 8 }); if (x != LOCATION_NULL) { SetState(PEEP_STATE_FALLING); action = PEEP_ACTION_NONE_2; special_sprite = 0; action_sprite_image_offset = 0; action_sprite_type = PEEP_ACTION_SPRITE_TYPE_NONE; path_check_optimisation = 0; } gPickupPeepImage = UINT32_MAX; } // Returns true when a peep can be dropped at the given location. When apply is set to true the peep gets dropped. bool Peep::Place(const TileCoordsXYZ& location, bool apply) { auto* pathElement = map_get_path_element_at(location); TileElement* tileElement = reinterpret_cast(pathElement); if (!pathElement) { tileElement = reinterpret_cast(map_get_surface_element_at(location.ToCoordsXYZ())); } if (!tileElement) return false; // Set the coordinate of destination to be exactly // in the middle of a tile. CoordsXYZ destination = { location.ToCoordsXY().ToTileCentre(), tileElement->GetBaseZ() + 16 }; if (!map_is_location_owned(destination)) { gGameCommandErrorTitle = STR_ERR_CANT_PLACE_PERSON_HERE; return false; } if (!map_can_construct_at({ destination, destination.z, destination.z + (1 * 8) }, { 0b1111, 0 })) { if (gGameCommandErrorText != STR_RAISE_OR_LOWER_LAND_FIRST) { if (gGameCommandErrorText != STR_FOOTPATH_IN_THE_WAY) { gGameCommandErrorTitle = STR_ERR_CANT_PLACE_PERSON_HERE; return false; } } } if (apply) { MoveTo(destination); SetState(PEEP_STATE_FALLING); action = PEEP_ACTION_NONE_2; special_sprite = 0; action_sprite_image_offset = 0; action_sprite_type = PEEP_ACTION_SPRITE_TYPE_NONE; path_check_optimisation = 0; sprite_position_tween_reset(); if (type == PEEP_TYPE_GUEST) { action_sprite_type = PEEP_ACTION_SPRITE_TYPE_INVALID; happiness_target = std::max(happiness_target - 10, 0); UpdateCurrentActionSpriteType(); } } return true; } /** * * rct2: 0x0069A535 */ void peep_sprite_remove(Peep* peep) { auto guest = peep->AsGuest(); if (guest != nullptr) { guest->RemoveFromRide(); } peep->Invalidate(); window_close_by_number(WC_PEEP, peep->sprite_index); window_close_by_number(WC_FIRE_PROMPT, peep->sprite_identifier); if (peep->type == PEEP_TYPE_GUEST) { window_invalidate_by_class(WC_GUEST_LIST); news_item_disable_news(NEWS_ITEM_PEEP_ON_RIDE, peep->sprite_index); } else { window_invalidate_by_class(WC_STAFF_LIST); gStaffModes[peep->staff_id] = 0; peep->type = PEEP_TYPE_INVALID; staff_update_greyed_patrol_areas(); peep->type = PEEP_TYPE_STAFF; news_item_disable_news(NEWS_ITEM_PEEP, peep->sprite_index); } sprite_remove(peep); } /** * New function removes peep from park existence. Works with staff. */ void Peep::Remove() { if (type == PEEP_TYPE_GUEST) { if (outside_of_park == 0) { decrement_guests_in_park(); auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT); context_broadcast_intent(&intent); } if (state == PEEP_STATE_ENTERING_PARK) { decrement_guests_heading_for_park(); } } peep_sprite_remove(this); } /** * Falling and its subset drowning * rct2: 0x690028 */ void Peep::UpdateFalling() { if (action == PEEP_ACTION_DROWNING) { // Check to see if we are ready to drown. UpdateAction(); Invalidate(); if (action == PEEP_ACTION_DROWNING) return; if (gConfigNotifications.guest_died) { auto ft = Formatter::Common(); FormatNameTo(ft); news_item_add_to_queue(NEWS_ITEM_BLANK, STR_NEWS_ITEM_GUEST_DROWNED, x | (y << 16)); } gParkRatingCasualtyPenalty = std::min(gParkRatingCasualtyPenalty + 25, 1000); Remove(); return; } // If not drowning then falling. Note: peeps 'fall' after leaving a ride/enter the park. TileElement* tile_element = map_get_first_element_at({ 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() == TILE_ELEMENT_TYPE_PATH) { int32_t height = map_height_from_slope( { x, y }, tile_element->AsPath()->GetSlopeDirection(), tile_element->AsPath()->IsSloped()) + tile_element->GetBaseZ(); if (height < z - 1 || height > z + 4) 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() == TILE_ELEMENT_TYPE_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 }); auto guest = AsGuest(); if (guest != nullptr) { // Drop balloon if held peep_release_balloon(guest, height); } InsertNewThought(PEEP_THOUGHT_TYPE_DROWNING, PEEP_THOUGHT_ITEM_NONE); action = PEEP_ACTION_DROWNING; action_frame = 0; action_sprite_image_offset = 0; UpdateCurrentActionSpriteType(); peep_window_state_update(this); return; } } int32_t map_height = tile_element_height({ 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 else continue; } 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() != TILE_ELEMENT_TYPE_PATH) { SetNextFlags(0, false, true); } else { SetNextFlags(saved_map->AsPath()->GetSlopeDirection(), saved_map->AsPath()->IsSloped(), false); } SetState(PEEP_STATE_1); } /** * * rct2: 0x6902A2 */ void Peep::Update1() { if (!CheckForPath()) return; if (type == PEEP_TYPE_GUEST) { SetState(PEEP_STATE_WALKING); } else { SetState(PEEP_STATE_PATROLLING); } destination_x = x; destination_y = y; destination_tolerance = 10; direction = sprite_direction >> 3; } void Peep::SetState(PeepState new_state) { peep_decrement_num_riders(this); state = new_state; peep_window_state_update(this); } /** * * rct2: 0x690009 */ void Peep::UpdatePicked() { if (gCurrentTicks & 0x1F) return; sub_state++; if (sub_state == 13) { InsertNewThought(PEEP_THOUGHT_TYPE_HELP, PEEP_THOUGHT_ITEM_NONE); } } /* From peep_update */ static void peep_update_thoughts(Peep* peep) { // Thoughts must always have a gap of at least // 220 ticks in age between them. In order to // allow this when a thought is new it enters // a holding zone. Before it becomes fresh. int32_t add_fresh = 1; int32_t fresh_thought = -1; for (int32_t i = 0; i < PEEP_MAX_THOUGHTS; i++) { if (peep->thoughts[i].type == PEEP_THOUGHT_TYPE_NONE) break; if (peep->thoughts[i].freshness == 1) { add_fresh = 0; // If thought is fresh we wait 220 ticks // before allowing a new thought to become fresh. if (++peep->thoughts[i].fresh_timeout >= 220) { peep->thoughts[i].fresh_timeout = 0; // Thought is no longer fresh peep->thoughts[i].freshness++; add_fresh = 1; } } else if (peep->thoughts[i].freshness > 1) { if (++peep->thoughts[i].fresh_timeout == 0) { // When thought is older than ~6900 ticks remove it if (++peep->thoughts[i].freshness >= 28) { peep->window_invalidate_flags |= PEEP_INVALIDATE_PEEP_THOUGHTS; // Clear top thought, push others up if (i < PEEP_MAX_THOUGHTS - 2) { memmove( &peep->thoughts[i], &peep->thoughts[i + 1], sizeof(rct_peep_thought) * (PEEP_MAX_THOUGHTS - i - 1)); } peep->thoughts[PEEP_MAX_THOUGHTS - 1].type = PEEP_THOUGHT_TYPE_NONE; } } } else { fresh_thought = i; } } // If there are no fresh thoughts // a previously new thought can become // fresh. if (add_fresh && fresh_thought != -1) { peep->thoughts[fresh_thought].freshness = 1; peep->window_invalidate_flags |= PEEP_INVALIDATE_PEEP_THOUGHTS; } } /** * * rct2: 0x0068FC1E */ void Peep::Update() { if (type == PEEP_TYPE_GUEST) { if (previous_ride != RIDE_ID_NULL) if (++previous_ride_time_out >= 720) previous_ride = RIDE_ID_NULL; peep_update_thoughts(this); } // Walking speed logic uint32_t stepsToTake = energy; if (stepsToTake < 95 && state == PEEP_STATE_QUEUING) stepsToTake = 95; if ((peep_flags & PEEP_FLAGS_SLOW_WALK) && state != PEEP_STATE_QUEUING) stepsToTake /= 2; if (action == PEEP_ACTION_NONE_2 && (GetNextIsSloped())) { stepsToTake /= 2; if (state == PEEP_STATE_QUEUING) stepsToTake += stepsToTake / 2; } uint32_t carryCheck = step_progress + stepsToTake; step_progress = carryCheck; if (carryCheck <= 255) { auto guest = AsGuest(); if (guest != nullptr) { guest->UpdateEasterEggInteractions(); } } else { // loc_68FD2F switch (state) { case PEEP_STATE_FALLING: UpdateFalling(); break; case PEEP_STATE_1: Update1(); break; case PEEP_STATE_ON_RIDE: // No action break; case PEEP_STATE_PICKED: UpdatePicked(); break; default: { auto guest = AsGuest(); if (guest != nullptr) { guest->UpdateGuest(); } else { auto staff = AsStaff(); if (staff != nullptr) { staff->UpdateStaff(stepsToTake); } else { assert(false); } } break; } } } } /** * * rct2: 0x0069BF41 */ void peep_problem_warnings_update() { Peep* peep; Ride* ride; uint16_t spriteIndex; uint32_t hunger_counter = 0, lost_counter = 0, noexit_counter = 0, thirst_counter = 0, litter_counter = 0, disgust_counter = 0, toilet_counter = 0, vandalism_counter = 0; uint8_t* warning_throttle = gPeepWarningThrottle; FOR_ALL_GUESTS (spriteIndex, peep) { if (peep->outside_of_park != 0 || peep->thoughts[0].freshness > 5) continue; switch (peep->thoughts[0].type) { case PEEP_THOUGHT_TYPE_LOST: // 0x10 lost_counter++; break; case PEEP_THOUGHT_TYPE_HUNGRY: // 0x14 if (peep->guest_heading_to_ride_id == 0xFF) { hunger_counter++; break; } ride = get_ride(peep->guest_heading_to_ride_id); if (ride != nullptr && !ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_FLAT_RIDE)) hunger_counter++; break; case PEEP_THOUGHT_TYPE_THIRSTY: if (peep->guest_heading_to_ride_id == 0xFF) { thirst_counter++; break; } ride = get_ride(peep->guest_heading_to_ride_id); if (ride != nullptr && !ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_SELLS_DRINKS)) thirst_counter++; break; case PEEP_THOUGHT_TYPE_TOILET: if (peep->guest_heading_to_ride_id == 0xFF) { toilet_counter++; break; } ride = get_ride(peep->guest_heading_to_ride_id); if (ride != nullptr && !ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_IS_TOILET)) toilet_counter++; break; case PEEP_THOUGHT_TYPE_BAD_LITTER: // 0x1a litter_counter++; break; case PEEP_THOUGHT_TYPE_CANT_FIND_EXIT: // 0x1b noexit_counter++; break; case PEEP_THOUGHT_TYPE_PATH_DISGUSTING: // 0x1f disgust_counter++; break; case PEEP_THOUGHT_TYPE_VANDALISM: // 0x21 vandalism_counter++; break; default: break; } } // could maybe be packed into a loop, would lose a lot of clarity though if (warning_throttle[0]) --warning_throttle[0]; else if (hunger_counter >= PEEP_HUNGER_WARNING_THRESHOLD && hunger_counter >= gNumGuestsInPark / 16) { warning_throttle[0] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_ARE_HUNGRY, 20); } } if (warning_throttle[1]) --warning_throttle[1]; else if (thirst_counter >= PEEP_THIRST_WARNING_THRESHOLD && thirst_counter >= gNumGuestsInPark / 16) { warning_throttle[1] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_ARE_THIRSTY, 21); } } if (warning_throttle[2]) --warning_throttle[2]; else if (toilet_counter >= PEEP_TOILET_WARNING_THRESHOLD && toilet_counter >= gNumGuestsInPark / 16) { warning_throttle[2] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_CANT_FIND_TOILET, 22); } } if (warning_throttle[3]) --warning_throttle[3]; else if (litter_counter >= PEEP_LITTER_WARNING_THRESHOLD && litter_counter >= gNumGuestsInPark / 32) { warning_throttle[3] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_DISLIKE_LITTER, 26); } } if (warning_throttle[4]) --warning_throttle[4]; else if (disgust_counter >= PEEP_DISGUST_WARNING_THRESHOLD && disgust_counter >= gNumGuestsInPark / 32) { warning_throttle[4] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_DISGUSTED_BY_PATHS, 31); } } if (warning_throttle[5]) --warning_throttle[5]; else if (vandalism_counter >= PEEP_VANDALISM_WARNING_THRESHOLD && vandalism_counter >= gNumGuestsInPark / 32) { warning_throttle[5] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_DISLIKE_VANDALISM, 33); } } if (warning_throttle[6]) --warning_throttle[6]; else if (noexit_counter >= PEEP_NOEXIT_WARNING_THRESHOLD) { warning_throttle[6] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_GETTING_LOST_OR_STUCK, 27); } } else if (lost_counter >= PEEP_LOST_WARNING_THRESHOLD) { warning_throttle[6] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_GETTING_LOST_OR_STUCK, 16); } } } void peep_stop_crowd_noise() { if (_crowdSoundChannel != nullptr) { Mixer_Stop_Channel(_crowdSoundChannel); _crowdSoundChannel = nullptr; } } /** * * rct2: 0x006BD18A */ void peep_update_crowd_noise() { rct_viewport* viewport; uint16_t spriteIndex; Peep* peep; int32_t visiblePeeps; if (gGameSoundsOff) return; if (!gConfigSound.sound_enabled) return; if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) return; viewport = g_music_tracking_viewport; if (viewport == nullptr) return; // Count the number of peeps visible visiblePeeps = 0; FOR_ALL_GUESTS (spriteIndex, peep) { if (peep->sprite_left == LOCATION_NULL) continue; if (viewport->viewPos.x > peep->sprite_right) continue; if (viewport->viewPos.x + viewport->view_width < peep->sprite_left) continue; if (viewport->viewPos.y > peep->sprite_bottom) continue; if (viewport->viewPos.y + viewport->view_height < peep->sprite_top) continue; visiblePeeps += peep->state == PEEP_STATE_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) { Mixer_Stop_Channel(_crowdSoundChannel); _crowdSoundChannel = nullptr; } } else { int32_t volume; // 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) volume = 120 - std::min(visiblePeeps, 120); volume = volume * volume * volume * volume; volume = (((207360000 - volume) / viewport->zoom) - 207360000) / 65536 - 150; // Load and play crowd noise if needed and set volume if (_crowdSoundChannel == nullptr) { _crowdSoundChannel = Mixer_Play_Music(PATH_ID_CSS2, MIXER_LOOP_INFINITE, false); if (_crowdSoundChannel != nullptr) { Mixer_Channel_SetGroup(_crowdSoundChannel, MIXER_GROUP_SOUND); } } if (_crowdSoundChannel != nullptr) { Mixer_Channel_Volume(_crowdSoundChannel, DStoMixerVolume(volume)); } } } /** * * rct2: 0x0069BE9B */ void peep_applause() { uint16_t spriteIndex; Peep* p; FOR_ALL_GUESTS (spriteIndex, p) { auto peep = p->AsGuest(); assert(peep != nullptr); if (peep->outside_of_park != 0) continue; // Release balloon peep_release_balloon(peep, peep->z + 9); // Clap if ((peep->state == PEEP_STATE_WALKING || peep->state == PEEP_STATE_QUEUING) && peep->action >= 254) { peep->action = PEEP_ACTION_CLAP; peep->action_frame = 0; peep->action_sprite_image_offset = 0; peep->UpdateCurrentActionSpriteType(); } } // Play applause noise audio_play_sound(SoundId::Applause, 0, context_get_width() / 2); } /** * * rct2: 0x0069C35E */ void peep_update_days_in_queue() { uint16_t sprite_index; Peep* peep; FOR_ALL_GUESTS (sprite_index, peep) { if (peep->outside_of_park == 0 && peep->state == PEEP_STATE_QUEUING) { if (peep->DaysInQueue < 255) { peep->DaysInQueue += 1; } } } } // clang-format off /** rct2: 0x009823A0 */ static constexpr const enum PeepNauseaTolerance nausea_tolerance_distribution[] = { PEEP_NAUSEA_TOLERANCE_NONE, PEEP_NAUSEA_TOLERANCE_LOW, PEEP_NAUSEA_TOLERANCE_LOW, PEEP_NAUSEA_TOLERANCE_AVERAGE, PEEP_NAUSEA_TOLERANCE_AVERAGE, PEEP_NAUSEA_TOLERANCE_AVERAGE, PEEP_NAUSEA_TOLERANCE_HIGH, PEEP_NAUSEA_TOLERANCE_HIGH, PEEP_NAUSEA_TOLERANCE_HIGH, PEEP_NAUSEA_TOLERANCE_HIGH, PEEP_NAUSEA_TOLERANCE_HIGH, PEEP_NAUSEA_TOLERANCE_HIGH, }; /** rct2: 0x009823BC */ static constexpr const uint8_t trouser_colours[] = { COLOUR_BLACK, COLOUR_GREY, COLOUR_LIGHT_BROWN, COLOUR_SATURATED_BROWN, COLOUR_DARK_BROWN, COLOUR_SALMON_PINK, COLOUR_BLACK, COLOUR_GREY, COLOUR_LIGHT_BROWN, COLOUR_SATURATED_BROWN, COLOUR_DARK_BROWN, COLOUR_SALMON_PINK, COLOUR_BLACK, COLOUR_GREY, COLOUR_LIGHT_BROWN, COLOUR_SATURATED_BROWN, COLOUR_DARK_BROWN, COLOUR_SALMON_PINK, COLOUR_DARK_PURPLE, COLOUR_LIGHT_PURPLE, COLOUR_DARK_BLUE, COLOUR_SATURATED_GREEN, COLOUR_SATURATED_RED, COLOUR_DARK_ORANGE, COLOUR_BORDEAUX_RED, }; /** rct2: 0x009823D5 */ static constexpr const uint8_t tshirt_colours[] = { COLOUR_BLACK, COLOUR_GREY, COLOUR_LIGHT_BROWN, COLOUR_SATURATED_BROWN, COLOUR_DARK_BROWN, COLOUR_SALMON_PINK, COLOUR_BLACK, COLOUR_GREY, COLOUR_LIGHT_BROWN, COLOUR_SATURATED_BROWN, COLOUR_DARK_BROWN, COLOUR_SALMON_PINK, COLOUR_DARK_PURPLE, COLOUR_LIGHT_PURPLE, COLOUR_DARK_BLUE, COLOUR_SATURATED_GREEN, COLOUR_SATURATED_RED, COLOUR_DARK_ORANGE, COLOUR_BORDEAUX_RED, COLOUR_WHITE, COLOUR_BRIGHT_PURPLE, COLOUR_LIGHT_BLUE, COLOUR_TEAL, COLOUR_DARK_GREEN, COLOUR_MOSS_GREEN, COLOUR_BRIGHT_GREEN, COLOUR_OLIVE_GREEN, COLOUR_DARK_OLIVE_GREEN, COLOUR_YELLOW, COLOUR_LIGHT_ORANGE, COLOUR_BRIGHT_RED, COLOUR_DARK_PINK, COLOUR_BRIGHT_PINK, }; // clang-format on /** * * rct2: 0x699F5A * al:thoughtType * ah:thoughtArguments * esi: peep */ void Peep::InsertNewThought(PeepThoughtType thoughtType, uint8_t thoughtArguments) { PeepActionType newAction = PeepThoughtToActionMap[thoughtType].action; if (newAction != PEEP_ACTION_NONE_2 && this->action >= PEEP_ACTION_NONE_1) { action = newAction; action_frame = 0; action_sprite_image_offset = 0; UpdateCurrentActionSpriteType(); } for (int32_t i = 0; i < PEEP_MAX_THOUGHTS; ++i) { rct_peep_thought* thought = &thoughts[i]; // Remove the oldest thought by setting it to NONE. if (thought->type == PEEP_THOUGHT_TYPE_NONE) break; if (thought->type == thoughtType && thought->item == thoughtArguments) { // If the thought type has not changed then we need to move // it to the top of the thought list. This is done by first removing the // existing thought and placing it at the top. if (i < PEEP_MAX_THOUGHTS - 2) { memmove(thought, thought + 1, sizeof(rct_peep_thought) * (PEEP_MAX_THOUGHTS - i - 1)); } break; } } memmove(&thoughts[1], &thoughts[0], sizeof(rct_peep_thought) * (PEEP_MAX_THOUGHTS - 1)); thoughts[0].type = thoughtType; thoughts[0].item = thoughtArguments; thoughts[0].freshness = 0; thoughts[0].fresh_timeout = 0; window_invalidate_flags |= PEEP_INVALIDATE_PEEP_THOUGHTS; } /** * * rct2: 0x0069A05D */ Peep* Peep::Generate(const CoordsXYZ& coords) { if (gSpriteListCount[SPRITE_LIST_FREE] < 400) return nullptr; Peep* peep = &create_sprite(SPRITE_IDENTIFIER_PEEP)->peep; peep->sprite_identifier = SPRITE_IDENTIFIER_PEEP; peep->sprite_type = PEEP_SPRITE_TYPE_NORMAL; peep->outside_of_park = 1; peep->state = PEEP_STATE_FALLING; peep->action = PEEP_ACTION_NONE_2; peep->special_sprite = 0; peep->action_sprite_image_offset = 0; peep->no_action_frame_num = 0; peep->action_sprite_type = PEEP_ACTION_SPRITE_TYPE_NONE; peep->peep_flags = 0; peep->FavouriteRide = RIDE_ID_NULL; peep->FavouriteRideRating = 0; const rct_sprite_bounds* spriteBounds = g_peep_animation_entries[peep->sprite_type].sprite_bounds; peep->sprite_width = spriteBounds[peep->action_sprite_type].sprite_width; peep->sprite_height_negative = spriteBounds[peep->action_sprite_type].sprite_height_negative; peep->sprite_height_positive = spriteBounds[peep->action_sprite_type].sprite_height_positive; peep->MoveTo(coords); peep->sprite_direction = 0; peep->mass = (scenario_rand() & 0x1F) + 45; peep->path_check_optimisation = 0; peep->interaction_ride_index = RIDE_ID_NULL; peep->type = PEEP_TYPE_GUEST; peep->previous_ride = RIDE_ID_NULL; peep->thoughts->type = PEEP_THOUGHT_TYPE_NONE; peep->window_invalidate_flags = 0; uint8_t intensityHighest = (scenario_rand() & 0x7) + 3; uint8_t intensityLowest = std::min(intensityHighest, static_cast(7)) - 3; if (intensityHighest >= 7) intensityHighest = 15; /* Check which intensity boxes are enabled * and apply the appropriate intensity settings. */ if (gParkFlags & PARK_FLAGS_PREF_LESS_INTENSE_RIDES) { if (gParkFlags & PARK_FLAGS_PREF_MORE_INTENSE_RIDES) { intensityLowest = 0; intensityHighest = 15; } else { intensityLowest = 0; intensityHighest = 4; } } else if (gParkFlags & PARK_FLAGS_PREF_MORE_INTENSE_RIDES) { intensityLowest = 9; intensityHighest = 15; } peep->intensity = IntensityRange(intensityLowest, intensityHighest); uint8_t nauseaTolerance = scenario_rand() & 0x7; if (gParkFlags & PARK_FLAGS_PREF_MORE_INTENSE_RIDES) { nauseaTolerance += 4; } peep->nausea_tolerance = nausea_tolerance_distribution[nauseaTolerance]; /* Scenario editor limits initial guest happiness to between 37..253. * To be on the safe side, assume the value could have been hacked * to any value 0..255. */ peep->happiness = gGuestInitialHappiness; /* Assume a default initial happiness of 0 is wrong and set * to 128 (50%) instead. */ if (gGuestInitialHappiness == 0) peep->happiness = 128; /* Initial value will vary by -15..16 */ int8_t happinessDelta = (scenario_rand() & 0x1F) - 15; /* Adjust by the delta, clamping at min=0 and max=255. */ peep->happiness = std::clamp(peep->happiness + happinessDelta, 0, PEEP_MAX_HAPPINESS); peep->happiness_target = peep->happiness; peep->nausea = 0; peep->nausea_target = 0; /* Scenario editor limits initial guest hunger to between 37..253. * To be on the safe side, assume the value could have been hacked * to any value 0..255. */ peep->hunger = gGuestInitialHunger; /* Initial value will vary by -15..16 */ int8_t hungerDelta = (scenario_rand() & 0x1F) - 15; /* Adjust by the delta, clamping at min=0 and max=255. */ peep->hunger = std::clamp(peep->hunger + hungerDelta, 0, PEEP_MAX_HUNGER); /* Scenario editor limits initial guest thirst to between 37..253. * To be on the safe side, assume the value could have been hacked * to any value 0..255. */ peep->thirst = gGuestInitialThirst; /* Initial value will vary by -15..16 */ int8_t thirstDelta = (scenario_rand() & 0x1F) - 15; /* Adjust by the delta, clamping at min=0 and max=255. */ peep->thirst = std::clamp(peep->thirst + thirstDelta, 0, PEEP_MAX_THIRST); peep->toilet = 0; peep->time_to_consume = 0; std::fill_n(peep->rides_been_on, 32, 0x00); peep->no_of_rides = 0; std::fill_n(peep->ride_types_been_on, 16, 0x00); peep->id = gNextGuestNumber++; peep->name = nullptr; money32 cash = (scenario_rand() & 0x3) * 100 - 100 + gGuestInitialCash; if (cash < 0) cash = 0; if (gGuestInitialCash == 0) { cash = 500; } if (gParkFlags & PARK_FLAGS_NO_MONEY) { cash = 0; } if (gGuestInitialCash == MONEY16_UNDEFINED) { cash = 0; } peep->cash_in_pocket = cash; peep->cash_spent = 0; peep->time_in_park = -1; peep->pathfind_goal.x = 0xFF; peep->pathfind_goal.y = 0xFF; peep->pathfind_goal.z = 0xFF; peep->pathfind_goal.direction = INVALID_DIRECTION; peep->ItemStandardFlags = 0; peep->item_extra_flags = 0; peep->guest_heading_to_ride_id = RIDE_ID_NULL; peep->LitterCount = 0; peep->DisgustingCount = 0; peep->VandalismSeen = 0; peep->PaidToEnter = 0; peep->PaidOnRides = 0; peep->PaidOnFood = 0; peep->paid_on_drink = 0; peep->PaidOnSouvenirs = 0; peep->AmountOfFood = 0; peep->AmountOfDrinks = 0; peep->AmountOfSouvenirs = 0; peep->SurroundingsThoughtTimeout = 0; peep->Angriness = 0; peep->TimeLost = 0; uint8_t tshirtColour = static_cast(scenario_rand() % std::size(tshirt_colours)); peep->tshirt_colour = tshirt_colours[tshirtColour]; uint8_t trousersColour = static_cast(scenario_rand() % std::size(trouser_colours)); peep->trousers_colour = trouser_colours[trousersColour]; /* Minimum energy is capped at 32 and maximum at 128, so this initialises * a peep with approx 34%-100% energy. (65 - 32) / (128 - 32) ≈ 34% */ uint8_t energy = (scenario_rand() % 64) + 65; peep->energy = energy; peep->energy_target = energy; increment_guests_heading_for_park(); return peep; } void Peep::FormatActionTo(Formatter& ft) const { FormatActionTo(ft.Buf()); } void Peep::FormatActionTo(void* argsV) const { Formatter ft(static_cast(argsV)); switch (state) { case PEEP_STATE_FALLING: ft.Add(action == PEEP_ACTION_DROWNING ? STR_DROWNING : STR_WALKING); break; case PEEP_STATE_1: ft.Add(STR_WALKING); break; case PEEP_STATE_ON_RIDE: case PEEP_STATE_LEAVING_RIDE: case PEEP_STATE_ENTERING_RIDE: { auto ride = get_ride(current_ride); if (ride != nullptr) { ft.Add(ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_IN_RIDE) ? STR_IN_RIDE : STR_ON_RIDE); ride->FormatNameTo(ft.Buf()); } else { ft.Add(STR_ON_RIDE).Add(STR_NONE); } break; } case PEEP_STATE_BUYING: { ft.Add(STR_AT_RIDE); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(ft.Buf()); } else { ft.Add(STR_NONE); } break; } case PEEP_STATE_WALKING: case PEEP_STATE_USING_BIN: if (guest_heading_to_ride_id != RIDE_ID_NULL) { auto ride = get_ride(guest_heading_to_ride_id); if (ride != nullptr) { ft.Add(STR_HEADING_FOR); ride->FormatNameTo(ft.Buf()); } } else { ft.Add((peep_flags & PEEP_FLAGS_LEAVING_PARK) ? STR_LEAVING_PARK : STR_WALKING); } break; case PEEP_STATE_QUEUING_FRONT: case PEEP_STATE_QUEUING: { auto ride = get_ride(current_ride); if (ride != nullptr) { ft.Add(STR_QUEUING_FOR); ride->FormatNameTo(ft.Buf()); } break; } case PEEP_STATE_SITTING: ft.Add(STR_SITTING); break; case PEEP_STATE_WATCHING: if (current_ride != RIDE_ID_NULL) { auto ride = get_ride(current_ride); if (ride != nullptr) { ft.Add((current_seat & 0x1) ? STR_WATCHING_CONSTRUCTION_OF : STR_WATCHING_RIDE); ride->FormatNameTo(ft.Buf()); } } else { ft.Add((current_seat & 0x1) ? STR_WATCHING_NEW_RIDE_BEING_CONSTRUCTED : STR_LOOKING_AT_SCENERY); } break; case PEEP_STATE_PICKED: ft.Add(STR_SELECT_LOCATION); break; case PEEP_STATE_PATROLLING: case PEEP_STATE_ENTERING_PARK: case PEEP_STATE_LEAVING_PARK: ft.Add(STR_WALKING); break; case PEEP_STATE_MOWING: ft.Add(STR_MOWING_GRASS); break; case PEEP_STATE_SWEEPING: ft.Add(STR_SWEEPING_FOOTPATH); break; case PEEP_STATE_WATERING: ft.Add(STR_WATERING_GARDENS); break; case PEEP_STATE_EMPTYING_BIN: ft.Add(STR_EMPTYING_LITTER_BIN); break; case PEEP_STATE_ANSWERING: if (sub_state == 0) { ft.Add(STR_WALKING); } else if (sub_state == 1) { ft.Add(STR_ANSWERING_RADIO_CALL); } else { ft.Add(STR_RESPONDING_TO_RIDE_BREAKDOWN_CALL); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(ft.Buf()); } else { ft.Add(STR_NONE); } } break; case PEEP_STATE_FIXING: { ft.Add(STR_FIXING_RIDE); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(ft.Buf()); } else { ft.Add(STR_NONE); } break; } case PEEP_STATE_HEADING_TO_INSPECTION: { ft.Add(STR_HEADING_TO_RIDE_FOR_INSPECTION); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(ft.Buf()); } else { ft.Add(STR_NONE); } break; } case PEEP_STATE_INSPECTING: { ft.Add(STR_INSPECTING_RIDE); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(ft.Buf()); } else { ft.Add(STR_NONE); } break; } } } void Peep::FormatNameTo(Formatter& ft) const { ft.Increment(FormatNameTo(ft.Buf())); } size_t Peep::FormatNameTo(void* argsV) const { Formatter ft(static_cast(argsV)); if (name == nullptr) { if (type == PeepType::PEEP_TYPE_STAFF) { static constexpr const rct_string_id staffNames[] = { STR_HANDYMAN_X, STR_MECHANIC_X, STR_SECURITY_GUARD_X, STR_ENTERTAINER_X, }; auto staffNameIndex = staff_type; if (staffNameIndex > sizeof(staffNames)) { staffNameIndex = 0; } ft.Add(staffNames[staffNameIndex]); ft.Add(id); return ft.NumBytes(); } else if (gParkFlags & PARK_FLAGS_SHOW_REAL_GUEST_NAMES) { auto realNameStringId = get_real_name_string_id_from_id(id); return ft.Add(realNameStringId).NumBytes(); } else { return ft.Add(STR_GUEST_X).Add(id).NumBytes(); } } else { return ft.Add(STR_STRING).Add(name).NumBytes(); } } std::string Peep::GetName() const { uint8_t args[32]{}; FormatNameTo(args); return format_string(STR_STRINGID, args); } bool Peep::SetName(const std::string_view& value) { if (value.empty()) { std::free(name); name = nullptr; return true; } else { auto newNameMemory = static_cast(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; } /** * rct2: 0x00698342 * thought.item (eax) * thought.type (ebx) * argument_1 (esi & ebx) * argument_2 (esi+2) */ void peep_thought_set_format_args(const rct_peep_thought* thought) { auto ft = Formatter::Common(); ft.Add(PeepThoughts[thought->type]); PeepThoughtToActionFlag flags = PeepThoughtToActionMap[thought->type].flags; if (flags & PEEP_THOUGHT_ACTION_FLAG_RIDE) { auto ride = get_ride(thought->item); if (ride != nullptr) { ride->FormatNameTo(ft); } else { ft.Add(STR_NONE); } } else if (flags & PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_SINGULAR) { ft.Add(ShopItems[thought->item].Naming.Singular); } else if (flags & PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE) { ft.Add(ShopItems[thought->item].Naming.Indefinite); } } /** rct2: 0x00982004 */ static constexpr const bool peep_allow_pick_up[] = { true, // PEEP_STATE_FALLING false, // PEEP_STATE_1 false, // PEEP_STATE_QUEUING_FRONT false, // PEEP_STATE_ON_RIDE false, // PEEP_STATE_LEAVING_RIDE true, // PEEP_STATE_WALKING true, // PEEP_STATE_QUEUING false, // PEEP_STATE_ENTERING_RIDE true, // PEEP_STATE_SITTING true, // PEEP_STATE_PICKED true, // PEEP_STATE_PATROLLING true, // PEEP_STATE_MOWING true, // PEEP_STATE_SWEEPING false, // PEEP_STATE_ENTERING_PARK false, // PEEP_STATE_LEAVING_PARK true, // PEEP_STATE_ANSWERING false, // PEEP_STATE_FIXING false, // PEEP_STATE_BUYING true, // PEEP_STATE_WATCHING true, // PEEP_STATE_EMPTYING_BIN true, // PEEP_STATE_USING_BIN true, // PEEP_STATE_WATERING true, // PEEP_STATE_HEADING_TO_INSPECTION false, // PEEP_STATE_INSPECTING }; /** * * rct2: 0x00698827 * returns 1 on pickup (CF not set) */ bool peep_can_be_picked_up(Peep* peep) { return peep_allow_pick_up[peep->state]; } enum { PEEP_FACE_OFFSET_ANGRY = 0, PEEP_FACE_OFFSET_VERY_VERY_SICK, PEEP_FACE_OFFSET_VERY_SICK, PEEP_FACE_OFFSET_SICK, PEEP_FACE_OFFSET_VERY_TIRED, PEEP_FACE_OFFSET_TIRED, PEEP_FACE_OFFSET_VERY_VERY_UNHAPPY, PEEP_FACE_OFFSET_VERY_UNHAPPY, PEEP_FACE_OFFSET_UNHAPPY, PEEP_FACE_OFFSET_NORMAL, PEEP_FACE_OFFSET_HAPPY, PEEP_FACE_OFFSET_VERY_HAPPY, PEEP_FACE_OFFSET_VERY_VERY_HAPPY, }; static constexpr const int32_t face_sprite_small[] = { SPR_PEEP_SMALL_FACE_ANGRY, SPR_PEEP_SMALL_FACE_VERY_VERY_SICK, SPR_PEEP_SMALL_FACE_VERY_SICK, SPR_PEEP_SMALL_FACE_SICK, SPR_PEEP_SMALL_FACE_VERY_TIRED, SPR_PEEP_SMALL_FACE_TIRED, SPR_PEEP_SMALL_FACE_VERY_VERY_UNHAPPY, SPR_PEEP_SMALL_FACE_VERY_UNHAPPY, SPR_PEEP_SMALL_FACE_UNHAPPY, SPR_PEEP_SMALL_FACE_NORMAL, SPR_PEEP_SMALL_FACE_HAPPY, SPR_PEEP_SMALL_FACE_VERY_HAPPY, SPR_PEEP_SMALL_FACE_VERY_VERY_HAPPY, }; static constexpr const int32_t face_sprite_large[] = { SPR_PEEP_LARGE_FACE_ANGRY_0, SPR_PEEP_LARGE_FACE_VERY_VERY_SICK_0, SPR_PEEP_LARGE_FACE_VERY_SICK_0, SPR_PEEP_LARGE_FACE_SICK, SPR_PEEP_LARGE_FACE_VERY_TIRED, SPR_PEEP_LARGE_FACE_TIRED, SPR_PEEP_LARGE_FACE_VERY_VERY_UNHAPPY, SPR_PEEP_LARGE_FACE_VERY_UNHAPPY, SPR_PEEP_LARGE_FACE_UNHAPPY, SPR_PEEP_LARGE_FACE_NORMAL, SPR_PEEP_LARGE_FACE_HAPPY, SPR_PEEP_LARGE_FACE_VERY_HAPPY, SPR_PEEP_LARGE_FACE_VERY_VERY_HAPPY, }; static int32_t get_face_sprite_offset(Peep* peep) { // ANGRY if (peep->Angriness > 0) return PEEP_FACE_OFFSET_ANGRY; // VERY_VERY_SICK if (peep->nausea > 200) return PEEP_FACE_OFFSET_VERY_VERY_SICK; // VERY_SICK if (peep->nausea > 170) return PEEP_FACE_OFFSET_VERY_SICK; // SICK if (peep->nausea > 140) return PEEP_FACE_OFFSET_SICK; // VERY_TIRED if (peep->energy < 46) return PEEP_FACE_OFFSET_VERY_TIRED; // TIRED if (peep->energy < 70) return PEEP_FACE_OFFSET_TIRED; int32_t offset = PEEP_FACE_OFFSET_VERY_VERY_UNHAPPY; // There are 7 different happiness based faces for (int32_t i = 37; peep->happiness >= i; i += 37) { offset++; } return offset; } /** * Function split into large and small sprite * rct2: 0x00698721 */ int32_t get_peep_face_sprite_small(Peep* peep) { return face_sprite_small[get_face_sprite_offset(peep)]; } /** * Function split into large and small sprite * rct2: 0x00698721 */ int32_t get_peep_face_sprite_large(Peep* peep) { return face_sprite_large[get_face_sprite_offset(peep)]; } void peep_set_map_tooltip(Peep* peep) { auto ft = Formatter::MapTooltip(); if (peep->type == PEEP_TYPE_GUEST) { ft.Add((peep->peep_flags & PEEP_FLAGS_TRACKING) ? STR_TRACKED_GUEST_MAP_TIP : STR_GUEST_MAP_TIP); ft.Add(get_peep_face_sprite_small(peep)); peep->FormatNameTo(ft); peep->FormatActionTo(ft); } else { ft.Add(STR_STAFF_MAP_TIP); peep->FormatNameTo(ft); peep->FormatActionTo(ft); } } /** * rct2: 0x00693BAB */ void Peep::SwitchNextActionSpriteType() { // TBD: Add nextActionSpriteType as function parameter and make peep->next_action_sprite_type obsolete? if (next_action_sprite_type != action_sprite_type) { Invalidate(); action_sprite_type = next_action_sprite_type; const rct_sprite_bounds* spriteBounds = g_peep_animation_entries[sprite_type].sprite_bounds; sprite_width = spriteBounds[next_action_sprite_type].sprite_width; sprite_height_negative = spriteBounds[next_action_sprite_type].sprite_height_negative; sprite_height_positive = spriteBounds[next_action_sprite_type].sprite_height_positive; Invalidate(); } } /** * * rct2: 0x00693CBB */ static bool peep_update_queue_position(Peep* peep, uint8_t previous_action) { peep->time_in_queue++; if (peep->next_in_queue == SPRITE_INDEX_NULL) return false; Peep* peep_next = GET_PEEP(peep->next_in_queue); int16_t x_diff = abs(peep_next->x - peep->x); int16_t y_diff = abs(peep_next->y - peep->y); int16_t z_diff = abs(peep_next->z - peep->z); if (z_diff > 10) return false; if (x_diff < y_diff) { int16_t temp_x = x_diff; x_diff = y_diff; y_diff = temp_x; } x_diff += y_diff / 2; if (x_diff > 7) { if (x_diff > 13) { if ((peep->x & 0xFFE0) != (peep_next->x & 0xFFE0) || (peep->y & 0xFFE0) != (peep_next->y & 0xFFE0)) return false; } if (peep->sprite_direction != peep_next->sprite_direction) return false; switch (peep_next->sprite_direction / 8) { case 0: if (peep->x >= peep_next->x) return false; break; case 1: if (peep->y <= peep_next->y) return false; break; case 2: if (peep->x <= peep_next->x) return false; break; case 3: if (peep->y >= peep_next->y) return false; break; } } if (peep->action < PEEP_ACTION_NONE_1) peep->UpdateAction(); if (peep->action != PEEP_ACTION_NONE_2) return true; peep->action = PEEP_ACTION_NONE_1; peep->next_action_sprite_type = PEEP_ACTION_SPRITE_TYPE_WATCH_RIDE; if (previous_action != PEEP_ACTION_NONE_1) peep->Invalidate(); return true; } /** * * rct2: 0x00693EF2 */ static void peep_return_to_centre_of_tile(Peep* peep) { peep->direction = direction_reverse(peep->direction); peep->destination_x = (peep->x & 0xFFE0) + 16; peep->destination_y = (peep->y & 0xFFE0) + 16; peep->destination_tolerance = 5; } /** * * rct2: 0x00693f2C */ static void peep_interact_with_entrance(Peep* peep, int16_t x, int16_t y, TileElement* tile_element, uint8_t& pathing_result) { uint8_t entranceType = tile_element->AsEntrance()->GetEntranceType(); // 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->interaction_ride_index = 0xFF; peep_return_to_centre_of_tile(peep); return; } if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE) { auto rideIndex = tile_element->AsEntrance()->GetRideIndex(); auto ride = get_ride(rideIndex); if (ride == nullptr) return; auto guest = peep->AsGuest(); if (guest == nullptr) { // Default staff behaviour attempting to enter a // ride entrance is to turn around. peep->interaction_ride_index = 0xFF; peep_return_to_centre_of_tile(peep); return; } if (peep->state == PEEP_STATE_QUEUING) { // Guest is in the ride queue. peep->sub_state = 11; peep->action_sprite_image_offset = _unk_F1AEF0; return; } // Guest is on a normal path, i.e. ride has no queue. if (peep->interaction_ride_index == 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. peep_return_to_centre_of_tile(peep); return; } peep->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. peep->interaction_ride_index = rideIndex; peep_return_to_centre_of_tile(peep); return; } // Guest has decided to go on the ride. peep->action_sprite_image_offset = _unk_F1AEF0; peep->interaction_ride_index = rideIndex; uint16_t previous_last = ride->stations[stationNum].LastPeepInQueue; ride->stations[stationNum].LastPeepInQueue = peep->sprite_index; peep->next_in_queue = previous_last; ride->stations[stationNum].QueueLength++; peep->current_ride = rideIndex; peep->current_ride_station = stationNum; peep->DaysInQueue = 0; peep->SetState(PEEP_STATE_QUEUING); peep->sub_state = 11; peep->time_in_queue = 0; if (peep->peep_flags & PEEP_FLAGS_TRACKING) { auto ft = Formatter::Common(); peep->FormatNameTo(ft); ride->FormatNameTo(ft); if (gConfigNotifications.guest_queuing_for_ride) { news_item_add_to_queue(NEWS_ITEM_PEEP_ON_RIDE, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, peep->sprite_index); } } } else { // PARK_ENTRANCE auto guest = peep->AsGuest(); if (guest == nullptr) { // Staff cannot leave the park, so go back. peep_return_to_centre_of_tile(peep); return; } // If not the centre of the entrance arch if (tile_element->AsEntrance()->GetSequenceIndex() != 0) { peep_return_to_centre_of_tile(peep); return; } uint8_t entranceDirection = tile_element->GetDirection(); if (entranceDirection != peep->direction) { if (direction_reverse(entranceDirection) != peep->direction) { peep_return_to_centre_of_tile(peep); return; } // Peep is leaving the park. if (peep->state != PEEP_STATE_WALKING) { peep_return_to_centre_of_tile(peep); return; } if (!(peep->peep_flags & PEEP_FLAGS_LEAVING_PARK)) { // If the park is open and leaving flag isn't set return to centre if (gParkFlags & PARK_FLAGS_PARK_OPEN) { peep_return_to_centre_of_tile(peep); return; } } peep->destination_x += CoordsDirectionDelta[peep->direction].x; peep->destination_y += CoordsDirectionDelta[peep->direction].y; peep->destination_tolerance = 9; peep->MoveTo({ x, y, peep->z }); peep->SetState(PEEP_STATE_LEAVING_PARK); peep->var_37 = 0; if (peep->peep_flags & PEEP_FLAGS_TRACKING) { auto ft = Formatter::Common(); peep->FormatNameTo(ft); if (gConfigNotifications.guest_left_park) { news_item_add_to_queue(NEWS_ITEM_PEEP_ON_RIDE, STR_PEEP_TRACKING_LEFT_PARK, peep->sprite_index); } } return; } // Peep is entering the park. if (peep->state != PEEP_STATE_ENTERING_PARK) { peep_return_to_centre_of_tile(peep); return; } if (!(gParkFlags & PARK_FLAGS_PARK_OPEN)) { peep->state = PEEP_STATE_LEAVING_PARK; peep->var_37 = 1; decrement_guests_heading_for_park(); peep_window_state_update(peep); peep_return_to_centre_of_tile(peep); return; } bool found = false; auto entrance = std::find_if(gParkEntrances.begin(), gParkEntrances.end(), [x, y](const auto& e) { return e.x == floor2(x, 32) && e.y == floor2(y, 32); }); if (entrance != gParkEntrances.end()) { int16_t z = entrance->z / 8; entranceDirection = entrance->direction; int16_t next_x = (x & 0xFFE0) + CoordsDirectionDelta[entranceDirection].x; int16_t next_y = (y & 0xFFE0) + CoordsDirectionDelta[entranceDirection].y; // Make sure there is a path right behind the entrance, otherwise turn around TileElement* nextTileElement = map_get_first_element_at({ next_x, next_y }); do { if (nextTileElement == nullptr) break; if (nextTileElement->GetType() != TILE_ELEMENT_TYPE_PATH) continue; if (nextTileElement->AsPath()->IsQueue()) continue; if (nextTileElement->AsPath()->IsSloped()) { uint8_t slopeDirection = nextTileElement->AsPath()->GetSlopeDirection(); if (slopeDirection == entranceDirection) { if (z != nextTileElement->base_height) { continue; } found = true; break; } if (direction_reverse(slopeDirection) != entranceDirection) continue; if (z - 2 != nextTileElement->base_height) continue; found = true; break; } else { if (z != nextTileElement->base_height) { continue; } found = true; break; } } while (!(nextTileElement++)->IsLastForTile()); } if (!found) { peep->state = PEEP_STATE_LEAVING_PARK; peep->var_37 = 1; decrement_guests_heading_for_park(); peep_window_state_update(peep); peep_return_to_centre_of_tile(peep); return; } money16 entranceFee = park_get_entrance_fee(); if (entranceFee != 0) { if (peep->ItemStandardFlags & PEEP_ITEM_VOUCHER) { if (peep->VoucherType == VOUCHER_TYPE_PARK_ENTRY_HALF_PRICE) { entranceFee /= 2; peep->ItemStandardFlags &= ~PEEP_ITEM_VOUCHER; peep->window_invalidate_flags |= PEEP_INVALIDATE_PEEP_INVENTORY; } else if (peep->VoucherType == VOUCHER_TYPE_PARK_ENTRY_FREE) { entranceFee = 0; peep->ItemStandardFlags &= ~PEEP_ITEM_VOUCHER; peep->window_invalidate_flags |= PEEP_INVALIDATE_PEEP_INVENTORY; } } if (entranceFee > peep->cash_in_pocket) { peep->state = PEEP_STATE_LEAVING_PARK; peep->var_37 = 1; decrement_guests_heading_for_park(); peep_window_state_update(peep); peep_return_to_centre_of_tile(peep); return; } gTotalIncomeFromAdmissions += entranceFee; guest->SpendMoney(peep->PaidToEnter, entranceFee, ExpenditureType::ParkEntranceTickets); peep->peep_flags |= PEEP_FLAGS_HAS_PAID_FOR_PARK_ENTRY; } gTotalAdmissions++; window_invalidate_by_number(WC_PARK_INFORMATION, 0); peep->var_37 = 1; peep->destination_x += CoordsDirectionDelta[peep->direction].x; peep->destination_y += CoordsDirectionDelta[peep->direction].y; peep->destination_tolerance = 7; peep->MoveTo({ x, y, peep->z }); } } /** * * rct2: 0x006946D8 */ static void peep_footpath_move_forward(Peep* peep, int16_t x, int16_t y, TileElement* tile_element, bool vandalism) { peep->NextLoc = { CoordsXY{ x, y }.ToTileStart(), tile_element->GetBaseZ() }; peep->SetNextFlags(tile_element->AsPath()->GetSlopeDirection(), tile_element->AsPath()->IsSloped(), false); int16_t z = peep->GetZOnSlope(x, y); if (peep->type == PEEP_TYPE_STAFF) { peep->MoveTo({ x, y, z }); return; } uint8_t vandalThoughtTimeout = (peep->VandalismSeen & 0xC0) >> 6; // Advance the vandalised tiles by 1 uint8_t vandalisedTiles = (peep->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 ((scenario_rand() & 0xFFFF) <= 10922) { peep->InsertNewThought(PEEP_THOUGHT_TYPE_VANDALISM, PEEP_THOUGHT_ITEM_NONE); peep->happiness_target = std::max(0, peep->happiness_target - 17); } vandalThoughtTimeout = 3; } } if (vandalThoughtTimeout && (scenario_rand() & 0xFFFF) <= 4369) { vandalThoughtTimeout--; } peep->VandalismSeen = (vandalThoughtTimeout << 6) | vandalisedTiles; uint16_t crowded = 0; uint8_t litter_count = 0; uint8_t sick_count = 0; uint16_t sprite_id = sprite_get_first_in_quadrant(x, y); for (rct_sprite* sprite; sprite_id != SPRITE_INDEX_NULL; sprite_id = sprite->generic.next_in_quadrant) { sprite = get_sprite(sprite_id); if (sprite->IsPeep()) { Peep* other_peep = reinterpret_cast(sprite); if (other_peep->state != PEEP_STATE_WALKING) continue; if (abs(other_peep->z - peep->NextLoc.z) > 16) continue; crowded++; continue; } else if (sprite->generic.sprite_identifier == SPRITE_IDENTIFIER_LITTER) { Litter* litter = reinterpret_cast(sprite); if (abs(litter->z - peep->NextLoc.z) > 16) continue; litter_count++; if (litter->type != LITTER_TYPE_SICK && litter->type != LITTER_TYPE_SICK_ALT) continue; litter_count--; sick_count++; } } if (crowded >= 10 && peep->state == PEEP_STATE_WALKING && (scenario_rand() & 0xFFFF) <= 21845) { peep->InsertNewThought(PEEP_THOUGHT_TYPE_CROWDED, PEEP_THOUGHT_ITEM_NONE); peep->happiness_target = std::max(0, peep->happiness_target - 14); } litter_count = std::min(static_cast(3), litter_count); sick_count = std::min(static_cast(3), sick_count); uint8_t disgusting_time = peep->DisgustingCount & 0xC0; uint8_t disgusting_count = ((peep->DisgustingCount & 0xF) << 2) | sick_count; peep->DisgustingCount = disgusting_count | disgusting_time; if (disgusting_time & 0xC0 && (scenario_rand() & 0xFFFF) <= 4369) { // Reduce the disgusting time peep->DisgustingCount -= 0x40; } else { uint8_t total_sick = 0; for (uint8_t time = 0; time < 3; time++) { total_sick += (disgusting_count >> (2 * time)) & 0x3; } if (total_sick >= 3 && (scenario_rand() & 0xFFFF) <= 10922) { peep->InsertNewThought(PEEP_THOUGHT_TYPE_PATH_DISGUSTING, PEEP_THOUGHT_ITEM_NONE); peep->happiness_target = std::max(0, peep->happiness_target - 17); // Reset disgusting time peep->DisgustingCount |= 0xC0; } } uint8_t litter_time = peep->LitterCount & 0xC0; litter_count = ((peep->LitterCount & 0xF) << 2) | litter_count; peep->LitterCount = litter_count | litter_time; if (litter_time & 0xC0 && (scenario_rand() & 0xFFFF) <= 4369) { // Reduce the litter time peep->LitterCount -= 0x40; } else { uint8_t total_litter = 0; for (uint8_t time = 0; time < 3; time++) { total_litter += (litter_count >> (2 * time)) & 0x3; } if (total_litter >= 3 && (scenario_rand() & 0xFFFF) <= 10922) { peep->InsertNewThought(PEEP_THOUGHT_TYPE_BAD_LITTER, PEEP_THOUGHT_ITEM_NONE); peep->happiness_target = std::max(0, peep->happiness_target - 17); // Reset litter time peep->LitterCount |= 0xC0; } } peep->MoveTo({ x, y, z }); } /** * * rct2: 0x0069455E */ static void peep_interact_with_path(Peep* peep, int16_t x, int16_t y, TileElement* tile_element) { // 0x00F1AEE2 bool vandalism_present = false; if (tile_element->AsPath()->HasAddition() && (tile_element->AsPath()->IsBroken()) && (tile_element->AsPath()->GetEdges()) != 0xF) { vandalism_present = true; } int16_t z = tile_element->GetBaseZ(); if (map_is_location_owned({ x, y, z })) { if (peep->outside_of_park == 1) { peep_return_to_centre_of_tile(peep); return; } } else { if (peep->outside_of_park == 0) { peep_return_to_centre_of_tile(peep); return; } } auto guest = peep->AsGuest(); if (guest != nullptr && tile_element->AsPath()->IsQueue()) { auto rideIndex = tile_element->AsPath()->GetRideIndex(); if (peep->state == PEEP_STATE_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 (peep->current_ride == rideIndex) { peep_footpath_move_forward(peep, x, y, tile_element, vandalism_present); } else { // Queue got disconnected from the original ride. peep->interaction_ride_index = 0xFF; guest->RemoveFromQueue(); peep->SetState(PEEP_STATE_1); peep_footpath_move_forward(peep, x, y, tile_element, vandalism_present); } } else { // Peep is not queuing. peep->TimeLost = 0; auto stationNum = tile_element->AsPath()->GetStationIndex(); if ((tile_element->AsPath()->HasQueueBanner()) && (tile_element->AsPath()->GetQueueBannerDirection() == direction_reverse(peep->direction)) // 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 = get_ride(rideIndex); if (ride != nullptr && guest->ShouldGoOnRide(ride, stationNum, true, false)) { // Peep has decided to go on the ride at the queue. peep->interaction_ride_index = rideIndex; // Add the peep to the ride queue. uint16_t old_last_peep = ride->stations[stationNum].LastPeepInQueue; ride->stations[stationNum].LastPeepInQueue = peep->sprite_index; peep->next_in_queue = old_last_peep; ride->stations[stationNum].QueueLength++; peep_decrement_num_riders(peep); peep->current_ride = rideIndex; peep->current_ride_station = stationNum; peep->state = PEEP_STATE_QUEUING; peep->DaysInQueue = 0; peep_window_state_update(peep); peep->sub_state = 10; peep->destination_tolerance = 2; peep->time_in_queue = 0; if (peep->peep_flags & PEEP_FLAGS_TRACKING) { auto ft = Formatter::Common(); peep->FormatNameTo(ft); ride->FormatNameTo(ft); if (gConfigNotifications.guest_queuing_for_ride) { news_item_add_to_queue( NEWS_ITEM_PEEP_ON_RIDE, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, peep->sprite_index); } } peep_footpath_move_forward(peep, x, y, tile_element, vandalism_present); } else { // Peep has decided not to go on the ride. peep_return_to_centre_of_tile(peep); } } else { /* Peep is approaching a queue tile without a ride * sign facing the peep. */ peep_footpath_move_forward(peep, x, y, tile_element, vandalism_present); } } } else { peep->interaction_ride_index = 0xFF; if (peep->state == PEEP_STATE_QUEUING) { peep->RemoveFromQueue(); peep->SetState(PEEP_STATE_1); } peep_footpath_move_forward(peep, x, y, tile_element, vandalism_present); } } /** * * rct2: 0x00693F70 */ static bool peep_interact_with_shop(Peep* peep, int16_t x, int16_t y, TileElement* tile_element) { ride_id_t rideIndex = tile_element->AsTrack()->GetRideIndex(); auto ride = get_ride(rideIndex); if (ride == nullptr || !ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_IS_SHOP)) return false; auto guest = peep->AsGuest(); if (guest == nullptr) { peep_return_to_centre_of_tile(peep); return true; } peep->TimeLost = 0; if (ride->status != RIDE_STATUS_OPEN) { peep_return_to_centre_of_tile(peep); return true; } if (peep->interaction_ride_index == rideIndex) { peep_return_to_centre_of_tile(peep); return true; } if (peep->peep_flags & PEEP_FLAGS_LEAVING_PARK) { peep_return_to_centre_of_tile(peep); return true; } if (ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_PEEP_SHOULD_GO_INSIDE_FACILITY)) { peep->TimeLost = 0; if (!guest->ShouldGoOnRide(ride, 0, false, false)) { peep_return_to_centre_of_tile(peep); return true; } money16 cost = ride->price[0]; if (cost != 0 && !(gParkFlags & PARK_FLAGS_NO_MONEY)) { ride->total_profit += cost; ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME; // TODO: Refactor? SpendMoney previously accepted nullptr to not track money, passing a temporary variable as a // workaround money16 money = 0; guest->SpendMoney(money, cost, ExpenditureType::ParkRideTickets); } peep->destination_x = (x & 0xFFE0) + 16; peep->destination_y = (y & 0xFFE0) + 16; peep->destination_tolerance = 3; peep->current_ride = rideIndex; peep->SetState(PEEP_STATE_ENTERING_RIDE); peep->sub_state = PEEP_SHOP_APPROACH; peep->TimeOnRide = 0; ride->cur_num_customers++; if (peep->peep_flags & PEEP_FLAGS_TRACKING) { auto ft = Formatter::Common(); peep->FormatNameTo(ft); ride->FormatNameTo(ft); rct_string_id string_id = ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_IN_RIDE) ? STR_PEEP_TRACKING_PEEP_IS_IN_X : STR_PEEP_TRACKING_PEEP_IS_ON_X; if (gConfigNotifications.guest_used_facility) { news_item_add_to_queue(NEWS_ITEM_PEEP_ON_RIDE, string_id, peep->sprite_index); } } } else { if (peep->guest_heading_to_ride_id == rideIndex) peep->guest_heading_to_ride_id = 0xFF; peep->action_sprite_image_offset = _unk_F1AEF0; peep->SetState(PEEP_STATE_BUYING); peep->current_ride = rideIndex; peep->sub_state = 0; } return true; } bool is_valid_path_z_and_direction(TileElement* tileElement, int32_t currentZ, int32_t currentDirection) { if (tileElement->AsPath()->IsSloped()) { int32_t slopeDirection = tileElement->AsPath()->GetSlopeDirection(); if (slopeDirection == currentDirection) { if (currentZ != tileElement->base_height) return false; } else { slopeDirection = direction_reverse(slopeDirection); if (slopeDirection != currentDirection) return false; if (currentZ != tileElement->base_height + 2) return false; } } else { if (currentZ != tileElement->base_height) return false; } return true; } void Peep::PerformNextAction(uint8_t& pathing_result) { TileElement* tmpTile; PerformNextAction(pathing_result, tmpTile); } /** * * rct2: 0x00693C9E */ void Peep::PerformNextAction(uint8_t& pathing_result, TileElement*& tile_result) { pathing_result = 0; PeepActionType previousAction = action; if (action == PEEP_ACTION_NONE_1) action = PEEP_ACTION_NONE_2; if (state == PEEP_STATE_QUEUING) { if (peep_update_queue_position(this, previousAction)) return; } std::optional loc; if (!(loc = UpdateAction())) { pathing_result |= PATHING_DESTINATION_REACHED; uint8_t result = 0; auto guest = AsGuest(); if (guest != nullptr) { result = guest_path_finding(guest); } else { auto staff = AsStaff(); result = staff->DoPathFinding(); } if (result != 0) return; if (!(loc = UpdateAction())) return; } 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; } if (map_is_edge(newLoc)) { if (outside_of_park == 1) { pathing_result |= PATHING_OUTSIDE_PARK; } peep_return_to_centre_of_tile(this); return; } TileElement* tileElement = map_get_first_element_at(newLoc); if (tileElement == nullptr) return; int16_t base_z = std::max(0, (z / 8) - 2); int16_t top_z = (z / 8) + 1; do { if (base_z > tileElement->base_height) continue; if (top_z < tileElement->base_height) continue; if (tileElement->IsGhost()) continue; if (tileElement->GetType() == TILE_ELEMENT_TYPE_PATH) { peep_interact_with_path(this, newLoc.x, newLoc.y, tileElement); tile_result = tileElement; return; } else if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK) { if (peep_interact_with_shop(this, newLoc.x, newLoc.y, tileElement)) { tile_result = tileElement; return; } } else if (tileElement->GetType() == TILE_ELEMENT_TYPE_ENTRANCE) { peep_interact_with_entrance(this, newLoc.x, newLoc.y, tileElement, pathing_result); tile_result = tileElement; return; } } while (!(tileElement++)->IsLastForTile()); if (type == PEEP_TYPE_STAFF || (GetNextIsSurface())) { int16_t height = abs(tile_element_height(newLoc) - z); if (height <= 3 || (type == PEEP_TYPE_STAFF && height <= 32)) { interaction_ride_index = 0xFF; if (state == PEEP_STATE_QUEUING) { RemoveFromQueue(); SetState(PEEP_STATE_1); } if (!map_is_location_in_park(newLoc)) { peep_return_to_centre_of_tile(this); return; } auto surfaceElement = map_get_surface_element_at(newLoc); if (surfaceElement == nullptr) { peep_return_to_centre_of_tile(this); return; } int16_t water_height = surfaceElement->GetWaterHeight(); if (water_height > 0) { peep_return_to_centre_of_tile(this); return; } if (type == PEEP_TYPE_STAFF && !GetNextIsSurface()) { // Prevent staff from leaving the path on their own unless they're allowed to mow. if (!((this->staff_orders & STAFF_ORDERS_MOWING) && this->StaffMowingTimeout >= 12)) { peep_return_to_centre_of_tile(this); return; } } // 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; } } peep_return_to_centre_of_tile(this); } /** * * rct2: 0x0069A98C */ void peep_reset_pathfind_goal(Peep* peep) { #if defined(DEBUG_LEVEL_1) && DEBUG_LEVEL_1 if (gPathFindDebug) { log_info("Resetting pathfind_goal for %s", gPathFindDebugPeepName); } #endif // defined(DEBUG_LEVEL_1) && DEBUG_LEVEL_1 peep->pathfind_goal.x = 0xFF; peep->pathfind_goal.y = 0xFF; peep->pathfind_goal.z = 0xFF; peep->pathfind_goal.direction = INVALID_DIRECTION; } /** * 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 == LOCATION_NULL) return 0; if (GetNextIsSurface()) { return tile_element_height({ tile_x, tile_y }); } uint8_t slope = GetNextDirection(); return NextLoc.z + map_height_from_slope({ tile_x, tile_y }, slope, GetNextIsSloped()); } rct_string_id get_real_name_string_id_from_id(uint32_t id) { // Generate a name_string_idx from the peep id using bit twiddling uint16_t ax = static_cast(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 += REAL_NAME_START; return dx; } int32_t peep_compare(const uint16_t sprite_index_a, const uint16_t sprite_index_b) { Peep const* peep_a = GET_PEEP(sprite_index_a); Peep const* peep_b = GET_PEEP(sprite_index_b); // Compare types if (peep_a->type != peep_b->type) { return peep_a->type - peep_b->type; } if (peep_a->name == nullptr && peep_b->name == nullptr) { if (gParkFlags & 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->id - peep_b->id; } } // Compare their names as strings uint8_t args[32]{}; char nameA[256]{}; peep_a->FormatNameTo(args); format_string(nameA, sizeof(nameA), STR_STRINGID, args); char nameB[256]{}; peep_b->FormatNameTo(args); format_string(nameB, sizeof(nameB), STR_STRINGID, args); return strlogicalcmp(nameA, nameB); } /** * * rct2: 0x0069926C */ void peep_update_names(bool realNames) { if (realNames) { gParkFlags |= PARK_FLAGS_SHOW_REAL_GUEST_NAMES; // Peep names are now dynamic } else { gParkFlags &= ~PARK_FLAGS_SHOW_REAL_GUEST_NAMES; // Peep names are now dynamic } auto intent = Intent(INTENT_ACTION_REFRESH_GUEST_LIST); context_broadcast_intent(&intent); gfx_invalidate_screen(); } #if defined(DEBUG_LEVEL_1) && DEBUG_LEVEL_1 void pathfind_logging_enable([[maybe_unused]] Peep* peep) { # if defined(PATHFIND_DEBUG) && PATHFIND_DEBUG /* Determine if the pathfinding debugging is wanted for this peep. */ format_string(gPathFindDebugPeepName, sizeof(gPathFindDebugPeepName), peep->name_string_idx, &(peep->id)); /* For guests, use the existing PEEP_FLAGS_TRACKING flag to * determine for which guest(s) the pathfinding debugging will * be output for. */ if (peep->type == PEEP_TYPE_GUEST) { gPathFindDebug = peep->peep_flags & PEEP_FLAGS_TRACKING; } /* For staff, there is no tracking button (any other similar * suitable existing mechanism?), so fall back to a crude * string comparison with a compile time hardcoded name. */ else { gPathFindDebug = strcmp(gPathFindDebugPeepName, "Mechanic Debug") == 0; } # endif // defined(PATHFIND_DEBUG) && PATHFIND_DEBUG } void pathfind_logging_disable() { # if defined(PATHFIND_DEBUG) && PATHFIND_DEBUG gPathFindDebug = false; # endif // defined(PATHFIND_DEBUG) && PATHFIND_DEBUG } #endif // defined(DEBUG_LEVEL_1) && DEBUG_LEVEL_1 void increment_guests_in_park() { if (gNumGuestsInPark < UINT32_MAX) { gNumGuestsInPark++; } else { openrct2_assert(false, "Attempt to increment guests in park above max value (65535)."); } } void increment_guests_heading_for_park() { if (gNumGuestsHeadingForPark < UINT32_MAX) { gNumGuestsHeadingForPark++; } else { openrct2_assert(false, "Attempt to increment guests heading for park above max value (65535)."); } } void decrement_guests_in_park() { if (gNumGuestsInPark > 0) { gNumGuestsInPark--; } else { log_error("Attempt to decrement guests in park below zero."); } } void decrement_guests_heading_for_park() { if (gNumGuestsHeadingForPark > 0) { gNumGuestsHeadingForPark--; } else { log_error("Attempt to decrement guests heading for park below zero."); } } static void peep_release_balloon(Guest* peep, int16_t spawn_height) { if (peep->ItemStandardFlags & PEEP_ITEM_BALLOON) { peep->ItemStandardFlags &= ~PEEP_ITEM_BALLOON; if (peep->sprite_type == PEEP_SPRITE_TYPE_BALLOON && peep->x != LOCATION_NULL) { create_balloon(peep->x, peep->y, spawn_height, peep->BalloonColour, false); peep->window_invalidate_flags |= PEEP_INVALIDATE_PEEP_INVENTORY; peep->UpdateSpriteType(); } } } /** * * rct2: 0x006966A9 */ void Peep::RemoveFromQueue() { auto ride = get_ride(current_ride); if (ride == nullptr) return; auto& station = ride->stations[current_ride_station]; // Make sure we don't underflow, building while paused might reset it to 0 where peeps have // not yet left the queue. if (station.QueueLength > 0) { station.QueueLength--; } if (sprite_index == station.LastPeepInQueue) { station.LastPeepInQueue = next_in_queue; return; } auto spriteId = station.LastPeepInQueue; while (spriteId != SPRITE_INDEX_NULL) { Peep* other_peep = GET_PEEP(spriteId); if (sprite_index == other_peep->next_in_queue) { other_peep->next_in_queue = next_in_queue; return; } spriteId = other_peep->next_in_queue; } } /** * * rct2: 0x0069A512 */ void Peep::RemoveFromRide() { if (state == PEEP_STATE_QUEUING) { RemoveFromQueue(); } StateReset(); }