/***************************************************************************** * 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 "../actions/GameAction.h" #include "../audio/AudioMixer.h" #include "../audio/audio.h" #include "../config/Config.h" #include "../core/Guard.hpp" #include "../drawing/LightFX.h" #include "../entity/Balloon.h" #include "../entity/EntityRegistry.h" #include "../entity/EntityTweener.h" #include "../interface/Window.h" #include "../localisation/Formatter.h" #include "../localisation/Localisation.h" #include "../management/Finance.h" #include "../management/Marketing.h" #include "../management/NewsItem.h" #include "../network/network.h" #include "../paint/Paint.h" #include "../peep/GuestPathfinding.h" #include "../profiling/Profiling.h" #include "../ride/Ride.h" #include "../ride/RideData.h" #include "../ride/ShopItem.h" #include "../ride/Station.h" #include "../ride/Track.h" #include "../scenario/Scenario.h" #include "../sprites.h" #include "../util/Util.h" #include "../windows/Intent.h" #include "../world/Climate.h" #include "../world/ConstructionClearance.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/Surface.h" #include "PatrolArea.h" #include "Staff.h" #include #include #include 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]; 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); static PeepActionSpriteType PeepSpecialSpriteToSpriteTypeMap[] = { PeepActionSpriteType::None, PeepActionSpriteType::HoldMat, PeepActionSpriteType::StaffMower, }; static PeepActionSpriteType PeepActionToSpriteTypeMap[] = { PeepActionSpriteType::CheckTime, PeepActionSpriteType::EatFood, PeepActionSpriteType::ShakeHead, PeepActionSpriteType::EmptyPockets, PeepActionSpriteType::SittingEatFood, PeepActionSpriteType::SittingLookAroundLeft, PeepActionSpriteType::SittingLookAroundRight, PeepActionSpriteType::Wow, PeepActionSpriteType::ThrowUp, PeepActionSpriteType::Jump, PeepActionSpriteType::StaffSweep, PeepActionSpriteType::Drowning, PeepActionSpriteType::StaffAnswerCall, PeepActionSpriteType::StaffAnswerCall2, PeepActionSpriteType::StaffCheckboard, PeepActionSpriteType::StaffFix, PeepActionSpriteType::StaffFix2, PeepActionSpriteType::StaffFixGround, PeepActionSpriteType::StaffFix3, PeepActionSpriteType::StaffWatering, PeepActionSpriteType::Joy, PeepActionSpriteType::ReadMap, PeepActionSpriteType::Wave, PeepActionSpriteType::StaffEmptyBin, PeepActionSpriteType::Wave2, PeepActionSpriteType::TakePhoto, PeepActionSpriteType::Clap, PeepActionSpriteType::Disgust, PeepActionSpriteType::DrawPicture, PeepActionSpriteType::BeingWatched, PeepActionSpriteType::WithdrawMoney, }; 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, }; template<> bool EntityBase::Is() 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 peep_get_staff_count() { return GetEntityListCount(EntityType::Staff); } /** * * rct2: 0x0068F0A9 */ void peep_update_all() { PROFILED_FUNCTION(); if (gScreenFlags & SCREEN_FLAGS_EDITOR) return; int32_t i = 0; // Warning this loop can delete peeps for (auto peep : EntityList()) { if (static_cast(i & 0x7F) != (gCurrentTicks & 0x7F)) { peep->Update(); } else { peep_128_tick_update(peep, i); // 128 tick can delete so double check its not deleted if (peep->Type == EntityType::Guest) { peep->Update(); } } i++; } for (auto staff : EntityList()) { if (static_cast(i & 0x7F) != (gCurrentTicks & 0x7F)) { staff->Update(); } else { peep_128_tick_update(staff, i); // 128 tick can delete so double check its not deleted if (staff->Type == EntityType::Staff) { staff->Update(); } } i++; } } /** * * rct2: 0x0068F41A * Called every 128 ticks */ static void peep_128_tick_update(Peep* peep, int32_t index) { PROFILED_FUNCTION(); auto* guest = peep->As(); if (guest != nullptr) { guest->Tick128UpdateGuest(index); } else { auto* staff = peep->As(); 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() { PROFILED_FUNCTION(); PathCheckOptimisation++; if ((PathCheckOptimisation & 0xF) != (sprite_index.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 = map_get_first_element_at(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; } PeepActionSpriteType Peep::GetActionSpriteType() { if (IsActionInterruptable()) { // PeepActionType::None1 or PeepActionType::None2 return PeepSpecialSpriteToSpriteTypeMap[SpecialSprite]; } if (EnumValue(Action) < std::size(PeepActionToSpriteTypeMap)) { return PeepActionToSpriteTypeMap[EnumValue(Action)]; } openrct2_assert( EnumValue(Action) >= std::size(PeepActionToSpriteTypeMap) && Action < PeepActionType::Idle, "Invalid peep action %u", EnumValue(Action)); return PeepActionSpriteType::None; } /* * rct2: 0x00693B58 */ void Peep::UpdateCurrentActionSpriteType() { if (EnumValue(SpriteType) >= std::size(g_peep_animation_entries)) { return; } PeepActionSpriteType newActionSpriteType = GetActionSpriteType(); if (ActionSpriteType == newActionSpriteType) { return; } Invalidate(); ActionSpriteType = newActionSpriteType; const rct_sprite_bounds* spriteBounds = &GetSpriteBounds(SpriteType, ActionSpriteType); sprite_width = spriteBounds->sprite_width; sprite_height_negative = spriteBounds->sprite_height_negative; sprite_height_positive = spriteBounds->sprite_height_positive; Invalidate(); } /* rct2: 0x00693BE5 */ void Peep::SwitchToSpecialSprite(uint8_t special_sprite_id) { if (special_sprite_id == SpecialSprite) return; SpecialSprite = special_sprite_id; if (IsActionInterruptable()) { ActionSpriteImageOffset = 0; } UpdateCurrentActionSpriteType(); } void Peep::StateReset() { SetState(PeepState::One); 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) { PROFILED_FUNCTION(); _unk_F1AEF0 = ActionSpriteImageOffset; 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; if (IsActionWalking()) { if (xy_distance <= DestinationTolerance) { return std::nullopt; } int32_t nextDirection = 0; if (x_delta < y_delta) { nextDirection = 8; if (differenceLoc.y >= 0) { nextDirection = 24; } } else { nextDirection = 16; if (differenceLoc.x >= 0) { nextDirection = 0; } } sprite_direction = nextDirection; CoordsXY loc = { x, y }; loc += word_981D7C[nextDirection / 8]; WalkingFrameNum++; const rct_peep_animation* peepAnimation = &GetPeepAnimation(SpriteType); const uint8_t* imageOffset = peepAnimation[EnumValue(ActionSpriteType)].frame_offsets; if (WalkingFrameNum >= peepAnimation[EnumValue(ActionSpriteType)].num_frames) { WalkingFrameNum = 0; } ActionSpriteImageOffset = imageOffset[WalkingFrameNum]; return loc; } const rct_peep_animation* peepAnimation = &GetPeepAnimation(SpriteType); ActionFrame++; // If last frame of action if (ActionFrame >= peepAnimation[EnumValue(ActionSpriteType)].num_frames) { ActionSpriteImageOffset = 0; Action = PeepActionType::Walking; UpdateCurrentActionSpriteType(); return { { x, y } }; } ActionSpriteImageOffset = peepAnimation[EnumValue(ActionSpriteType)].frame_offsets[ActionFrame]; auto* guest = As(); // If not throwing up and not at the frame where sick appears. if (Action != PeepActionType::ThrowUp || ActionFrame != 15 || guest == nullptr) { return { { x, y } }; } // We are throwing up guest->Hunger /= 2; guest->NauseaTarget /= 2; if (guest->Nausea < 30) guest->Nausea = 0; else guest->Nausea -= 30; WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_2; const auto curLoc = GetLocation(); Litter::Create( { curLoc, sprite_direction }, (sprite_index.ToUnderlying() & 1) ? Litter::Type::VomitAlt : Litter::Type::Vomit); static constexpr OpenRCT2::Audio::SoundId coughs[4] = { OpenRCT2::Audio::SoundId::Cough1, OpenRCT2::Audio::SoundId::Cough2, OpenRCT2::Audio::SoundId::Cough3, OpenRCT2::Audio::SoundId::Cough4, }; auto soundId = coughs[scenario_rand() & 3]; OpenRCT2::Audio::Play3D(soundId, curLoc); return { { x, y } }; } /** * rct2: 0x0069A409 * Decreases rider count if on/entering a ride. */ void peep_decrement_num_riders(Peep* peep) { if (peep->State == PeepState::OnRide || peep->State == PeepState::EnteringRide) { auto ride = get_ride(peep->CurrentRide); 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.ToUnderlying()); if (w != nullptr) window_event_invalidate_call(w); if (peep->Is()) { if (peep->State == PeepState::OnRide || peep->State == PeepState::EnteringRide) { auto ride = get_ride(peep->CurrentRide); 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 = As(); if (guest != nullptr) { guest->RemoveFromRide(); } MoveTo({ LOCATION_NULL, 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 != LOCATION_NULL) { SetState(PeepState::Falling); Action = PeepActionType::Walking; SpecialSprite = 0; ActionSpriteImageOffset = 0; ActionSpriteType = PeepActionSpriteType::None; PathCheckOptimisation = 0; } gPickupPeepImage = ImageId(); } // Returns GameActions::Status::OK when a peep can be dropped at the given location. When apply is set to true the peep gets // dropped. GameActions::Result Peep::Place(const TileCoordsXYZ& location, bool apply) { auto* pathElement = map_get_path_element_at(location); TileElement* tileElement = reinterpret_cast(pathElement); if (pathElement == nullptr) { tileElement = reinterpret_cast(map_get_surface_element_at(location.ToCoordsXYZ())); } if (tileElement == nullptr) { return GameActions::Result(GameActions::Status::InvalidParameters, STR_ERR_CANT_PLACE_PERSON_HERE, STR_NONE); } // 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)) { 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(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; ActionSpriteImageOffset = 0; ActionSpriteType = PeepActionSpriteType::None; PathCheckOptimisation = 0; EntityTweener::Get().Reset(); auto* guest = As(); if (guest != nullptr) { ActionSpriteType = PeepActionSpriteType::Invalid; guest->HappinessTarget = std::max(guest->HappinessTarget - 10, 0); UpdateCurrentActionSpriteType(); } } return GameActions::Result(); } /** * * rct2: 0x0069A535 */ void peep_sprite_remove(Peep* peep) { auto* guest = peep->As(); if (guest != nullptr) { guest->RemoveFromRide(); } peep->Invalidate(); window_close_by_number(WC_PEEP, peep->sprite_index); window_close_by_number(WC_FIRE_PROMPT, EnumValue(peep->Type)); auto* staff = peep->As(); // Needed for invalidations after sprite removal bool wasGuest = staff == nullptr; if (wasGuest) { News::DisableNewsItems(News::ItemType::PeepOnRide, peep->sprite_index.ToUnderlying()); } else { staff->ClearPatrolArea(); UpdateConsolidatedPatrolAreas(); News::DisableNewsItems(News::ItemType::Peep, staff->sprite_index.ToUnderlying()); } EntityRemove(peep); auto intent = Intent(wasGuest ? INTENT_ACTION_REFRESH_GUEST_LIST : INTENT_ACTION_REFRESH_STAFF_LIST); context_broadcast_intent(&intent); } /** * New function removes peep from park existence. Works with staff. */ void Peep::Remove() { auto* guest = As(); if (guest != nullptr) { if (!guest->OutsideOfPark) { decrement_guests_in_park(); auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT); context_broadcast_intent(&intent); } if (State == PeepState::EnteringPark) { decrement_guests_heading_for_park(); } } peep_sprite_remove(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 (gConfigNotifications.guest_died) { auto ft = Formatter(); FormatNameTo(ft); News::AddItemToQueue(News::ItemType::Blank, STR_NEWS_ITEM_GUEST_DROWNED, x | (y << 16), ft); } 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(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 = 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() == 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 }); auto* guest = As(); if (guest != nullptr) { // Drop balloon if held peep_release_balloon(guest, height); guest->InsertNewThought(PeepThoughtType::Drowning); } Action = PeepActionType::Drowning; ActionFrame = 0; ActionSpriteImageOffset = 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() != 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()) { SetState(PeepState::Walking); } else { SetState(PeepState::Patrolling); } SetDestination(GetLocation(), 10); PeepDirection = 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; SubState++; auto* guest = As(); if (SubState == 13 && guest != nullptr) { guest->InsertNewThought(PeepThoughtType::Help); } } /* From peep_update */ static void peep_update_thoughts(Guest* 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 == PeepThoughtType::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->WindowInvalidateFlags |= 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(PeepThought) * (PEEP_MAX_THOUGHTS - i - 1)); } peep->Thoughts[PEEP_MAX_THOUGHTS - 1].type = PeepThoughtType::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->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_THOUGHTS; } } /** * * rct2: 0x0068FC1E */ void Peep::Update() { auto* guest = As(); if (guest != nullptr) { if (!guest->PreviousRide.IsNull()) if (++guest->PreviousRideTimeOut >= 720) guest->PreviousRide = RideId::GetNull(); peep_update_thoughts(guest); } // Walking speed logic uint32_t stepsToTake = Energy; if (stepsToTake < 95 && State == PeepState::Queuing) stepsToTake = 95; if ((PeepFlags & PEEP_FLAGS_SLOW_WALK) && State != PeepState::Queuing) stepsToTake /= 2; if (IsActionWalking() && GetNextIsSloped()) { stepsToTake /= 2; if (State == PeepState::Queuing) stepsToTake += stepsToTake / 2; } uint32_t carryCheck = StepProgress + stepsToTake; StepProgress = carryCheck; if (carryCheck <= 255) { if (guest != nullptr) { guest->UpdateEasterEggInteractions(); } } else { // loc_68FD2F switch (State) { case PeepState::Falling: UpdateFalling(); break; case PeepState::One: Update1(); break; case PeepState::OnRide: // No action break; case PeepState::Picked: UpdatePicked(); break; default: { if (guest != nullptr) { guest->UpdateGuest(); } else { auto* staff = As(); if (staff != nullptr) { staff->UpdateStaff(stepsToTake); } else { assert(false); } } break; } } } } /** * * rct2: 0x0069BF41 */ void peep_problem_warnings_update() { Ride* ride; 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 (auto peep : EntityList()) { if (peep->OutsideOfPark || peep->Thoughts[0].freshness > 5) continue; switch (peep->Thoughts[0].type) { case PeepThoughtType::Lost: // 0x10 lost_counter++; break; case PeepThoughtType::Hungry: // 0x14 if (peep->GuestHeadingToRideId.IsNull()) { hunger_counter++; break; } ride = get_ride(peep->GuestHeadingToRideId); if (ride != nullptr && !ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_FLAT_RIDE)) hunger_counter++; break; case PeepThoughtType::Thirsty: if (peep->GuestHeadingToRideId.IsNull()) { thirst_counter++; break; } ride = get_ride(peep->GuestHeadingToRideId); if (ride != nullptr && !ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_SELLS_DRINKS)) thirst_counter++; break; case PeepThoughtType::Toilet: if (peep->GuestHeadingToRideId.IsNull()) { toilet_counter++; break; } ride = get_ride(peep->GuestHeadingToRideId); if (ride != nullptr && !ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_TOILET)) toilet_counter++; break; case PeepThoughtType::BadLitter: // 0x1a litter_counter++; break; case PeepThoughtType::CantFindExit: // 0x1b noexit_counter++; break; case PeepThoughtType::PathDisgusting: // 0x1f disgust_counter++; break; case PeepThoughtType::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::AddItemToQueue(News::ItemType::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::AddItemToQueue(News::ItemType::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::AddItemToQueue(News::ItemType::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::AddItemToQueue(News::ItemType::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::AddItemToQueue(News::ItemType::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::AddItemToQueue(News::ItemType::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::AddItemToQueue(News::ItemType::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::AddItemToQueue(News::ItemType::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() { PROFILED_FUNCTION(); if (OpenRCT2::Audio::gGameSoundsOff) return; if (!gConfigSound.sound_enabled) return; if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) return; auto viewport = g_music_tracking_viewport; if (viewport == nullptr) return; // Count the number of peeps visible auto visiblePeeps = 0; for (auto peep : EntityList()) { if (peep->x == LOCATION_NULL) continue; if (viewport->viewPos.x > peep->SpriteRect.GetRight()) continue; if (viewport->viewPos.x + viewport->view_width < peep->SpriteRect.GetLeft()) continue; if (viewport->viewPos.y > peep->SpriteRect.GetBottom()) continue; if (viewport->viewPos.y + viewport->view_height < peep->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) { 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 = (viewport->zoom.ApplyInversedTo(207360000 - volume) - 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, OpenRCT2::Audio::MixerGroup::Sound); } } if (_crowdSoundChannel != nullptr) { Mixer_Channel_Volume(_crowdSoundChannel, DStoMixerVolume(volume)); } } } /** * * rct2: 0x0069BE9B */ void peep_applause() { for (auto peep : EntityList()) { if (peep->OutsideOfPark) continue; // Release balloon peep_release_balloon(peep, peep->z + 9); // Clap if ((peep->State == PeepState::Walking || peep->State == PeepState::Queuing) && peep->IsActionInterruptable()) { peep->Action = PeepActionType::Clap; peep->ActionFrame = 0; peep->ActionSpriteImageOffset = 0; peep->UpdateCurrentActionSpriteType(); } } // Play applause noise OpenRCT2::Audio::Play(OpenRCT2::Audio::SoundId::Applause, 0, context_get_width() / 2); } /** * * rct2: 0x0069C35E */ void peep_update_days_in_queue() { for (auto peep : EntityList()) { if (!peep->OutsideOfPark && peep->State == PeepState::Queuing) { if (peep->DaysInQueue < 255) { peep->DaysInQueue += 1; } } } } void Peep::FormatActionTo(Formatter& ft) const { switch (State) { case PeepState::Falling: ft.Add(Action == PeepActionType::Drowning ? STR_DROWNING : STR_WALKING); break; case PeepState::One: ft.Add(STR_WALKING); break; case PeepState::OnRide: case PeepState::LeavingRide: case PeepState::EnteringRide: { auto ride = get_ride(CurrentRide); if (ride != nullptr) { ft.Add( ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IN_RIDE) ? STR_IN_RIDE : STR_ON_RIDE); ride->FormatNameTo(ft); } else { ft.Add(STR_ON_RIDE).Add(STR_NONE); } break; } case PeepState::Buying: { ft.Add(STR_AT_RIDE); auto ride = get_ride(CurrentRide); if (ride != nullptr) { ride->FormatNameTo(ft); } else { ft.Add(STR_NONE); } break; } case PeepState::Walking: case PeepState::UsingBin: { auto* guest = As(); if (guest != nullptr) { if (!guest->GuestHeadingToRideId.IsNull()) { auto ride = get_ride(guest->GuestHeadingToRideId); if (ride != nullptr) { ft.Add(STR_HEADING_FOR); ride->FormatNameTo(ft); } } else { ft.Add((PeepFlags & PEEP_FLAGS_LEAVING_PARK) ? STR_LEAVING_PARK : STR_WALKING); } } break; } case PeepState::QueuingFront: case PeepState::Queuing: { auto ride = get_ride(CurrentRide); if (ride != nullptr) { ft.Add(STR_QUEUING_FOR); ride->FormatNameTo(ft); } break; } case PeepState::Sitting: ft.Add(STR_SITTING); break; case PeepState::Watching: if (!CurrentRide.IsNull()) { auto ride = get_ride(CurrentRide); if (ride != nullptr) { ft.Add((StandingFlags & 0x1) ? STR_WATCHING_CONSTRUCTION_OF : STR_WATCHING_RIDE); ride->FormatNameTo(ft); } } else { ft.Add((StandingFlags & 0x1) ? STR_WATCHING_NEW_RIDE_BEING_CONSTRUCTED : STR_LOOKING_AT_SCENERY); } break; case PeepState::Picked: ft.Add(STR_SELECT_LOCATION); break; case PeepState::Patrolling: case PeepState::EnteringPark: case PeepState::LeavingPark: ft.Add(STR_WALKING); break; case PeepState::Mowing: ft.Add(STR_MOWING_GRASS); break; case PeepState::Sweeping: ft.Add(STR_SWEEPING_FOOTPATH); break; case PeepState::Watering: ft.Add(STR_WATERING_GARDENS); break; case PeepState::EmptyingBin: ft.Add(STR_EMPTYING_LITTER_BIN); break; case PeepState::Answering: if (SubState == 0) { ft.Add(STR_WALKING); } else if (SubState == 1) { ft.Add(STR_ANSWERING_RADIO_CALL); } else { ft.Add(STR_RESPONDING_TO_RIDE_BREAKDOWN_CALL); auto ride = get_ride(CurrentRide); if (ride != nullptr) { ride->FormatNameTo(ft); } else { ft.Add(STR_NONE); } } break; case PeepState::Fixing: { ft.Add(STR_FIXING_RIDE); auto ride = get_ride(CurrentRide); if (ride != nullptr) { ride->FormatNameTo(ft); } else { ft.Add(STR_NONE); } break; } case PeepState::HeadingToInspection: { ft.Add(STR_HEADING_TO_RIDE_FOR_INSPECTION); auto ride = get_ride(CurrentRide); if (ride != nullptr) { ride->FormatNameTo(ft); } else { ft.Add(STR_NONE); } break; } case PeepState::Inspecting: { ft.Add(STR_INSPECTING_RIDE); auto ride = get_ride(CurrentRide); if (ride != nullptr) { ride->FormatNameTo(ft); } else { ft.Add(STR_NONE); } break; } } } static constexpr const rct_string_id _staffNames[] = { STR_HANDYMAN_X, STR_MECHANIC_X, STR_SECURITY_GUARD_X, STR_ENTERTAINER_X, }; void Peep::FormatNameTo(Formatter& ft) const { if (Name == nullptr) { auto* staff = As(); if (staff != nullptr) { auto staffNameIndex = static_cast(staff->AssignedStaffType); if (staffNameIndex >= std::size(_staffNames)) { staffNameIndex = 0; } ft.Add(_staffNames[staffNameIndex]); ft.Add(Id); } else if (gParkFlags & PARK_FLAGS_SHOW_REAL_GUEST_NAMES) { auto realNameStringId = get_real_name_string_id_from_id(Id); ft.Add(realNameStringId); } else { ft.Add(STR_GUEST_X).Add(Id); } } else { ft.Add(STR_STRING).Add(Name); } } std::string Peep::GetName() const { Formatter ft; FormatNameTo(ft); return format_string(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(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(); } void peep_set_map_tooltip(Peep* peep) { auto ft = Formatter(); auto* guest = peep->As(); if (guest != nullptr) { ft.Add((peep->PeepFlags & PEEP_FLAGS_TRACKING) ? STR_TRACKED_GUEST_MAP_TIP : STR_GUEST_MAP_TIP); ft.Add(get_peep_face_sprite_small(guest)); guest->FormatNameTo(ft); guest->FormatActionTo(ft); } else { ft.Add(STR_STAFF_MAP_TIP); peep->FormatNameTo(ft); peep->FormatActionTo(ft); } auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP); intent.putExtra(INTENT_EXTRA_FORMATTER, &ft); context_broadcast_intent(&intent); } /** * rct2: 0x00693BAB */ void Peep::SwitchNextActionSpriteType() { // TBD: Add nextActionSpriteType as function parameter and make peep->NextActionSpriteType obsolete? if (NextActionSpriteType != ActionSpriteType) { Invalidate(); ActionSpriteType = NextActionSpriteType; const rct_sprite_bounds* spriteBounds = &GetSpriteBounds(SpriteType, NextActionSpriteType); sprite_width = spriteBounds->sprite_width; sprite_height_negative = spriteBounds->sprite_height_negative; sprite_height_positive = spriteBounds->sprite_height_positive; Invalidate(); } } /** * * rct2: 0x00693EF2 */ static void peep_return_to_centre_of_tile(Peep* peep) { peep->PeepDirection = direction_reverse(peep->PeepDirection); auto destination = peep->GetLocation().ToTileCentre(); peep->SetDestination(destination, 5); } /** * * rct2: 0x00693f2C */ static bool peep_interact_with_entrance(Peep* peep, const CoordsXYE& coords, uint8_t& pathing_result) { auto tile_element = coords.element; uint8_t entranceType = tile_element->AsEntrance()->GetEntranceType(); auto rideIndex = tile_element->AsEntrance()->GetRideIndex(); if ((entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE) || (entranceType == ENTRANCE_TYPE_RIDE_EXIT)) { // If an entrance or exit that doesn't belong to the ride we are queuing for ignore the entrance/exit // This can happen when paths clip through entrance/exits if (peep->State == PeepState::Queuing && peep->CurrentRide != rideIndex) { return false; } } // Store some details to determine when to override the default // behaviour (defined below) for when staff attempt to enter a ride // to fix/inspect it. if (entranceType == ENTRANCE_TYPE_RIDE_EXIT) { pathing_result |= PATHING_RIDE_EXIT; _peepRideEntranceExitElement = tile_element; } else if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE) { pathing_result |= PATHING_RIDE_ENTRANCE; _peepRideEntranceExitElement = tile_element; } if (entranceType == ENTRANCE_TYPE_RIDE_EXIT) { // Default guest/staff behaviour attempting to enter a // ride exit is to turn around. peep->InteractionRideIndex = RideId::GetNull(); peep_return_to_centre_of_tile(peep); return true; } if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE) { auto ride = get_ride(rideIndex); if (ride == nullptr) return false; auto* guest = peep->As(); if (guest == nullptr) { // Default staff behaviour attempting to enter a // ride entrance is to turn around. peep->InteractionRideIndex = RideId::GetNull(); peep_return_to_centre_of_tile(peep); return true; } if (guest->State == PeepState::Queuing) { // Guest is in the ride queue. guest->RideSubState = PeepRideSubState::AtQueueFront; guest->ActionSpriteImageOffset = _unk_F1AEF0; 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. peep_return_to_centre_of_tile(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; peep_return_to_centre_of_tile(guest); return true; } // Guest has decided to go on the ride. guest->ActionSpriteImageOffset = _unk_F1AEF0; guest->InteractionRideIndex = rideIndex; auto& station = ride->GetStation(stationNum); auto previous_last = station.LastPeepInQueue; station.LastPeepInQueue = guest->sprite_index; guest->GuestNextInQueue = previous_last; station.QueueLength++; guest->CurrentRide = rideIndex; guest->CurrentRideStation = stationNum; guest->DaysInQueue = 0; guest->SetState(PeepState::Queuing); guest->RideSubState = PeepRideSubState::AtQueueFront; guest->TimeInQueue = 0; if (guest->PeepFlags & PEEP_FLAGS_TRACKING) { auto ft = Formatter(); guest->FormatNameTo(ft); ride->FormatNameTo(ft); if (gConfigNotifications.guest_queuing_for_ride) { News::AddItemToQueue( News::ItemType::PeepOnRide, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, guest->sprite_index, ft); } } } else { // PARK_ENTRANCE auto* guest = peep->As(); if (guest == nullptr) { // Staff cannot leave the park, so go back. peep_return_to_centre_of_tile(peep); return true; } // If not the centre of the entrance arch if (tile_element->AsEntrance()->GetSequenceIndex() != 0) { peep_return_to_centre_of_tile(guest); return true; } uint8_t entranceDirection = tile_element->GetDirection(); if (entranceDirection != guest->PeepDirection) { if (direction_reverse(entranceDirection) != guest->PeepDirection) { peep_return_to_centre_of_tile(guest); return true; } // Peep is leaving the park. if (guest->State != PeepState::Walking) { peep_return_to_centre_of_tile(guest); return true; } if (!(guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK)) { // If the park is open and leaving flag isn't set return to centre if (gParkFlags & PARK_FLAGS_PARK_OPEN) { peep_return_to_centre_of_tile(guest); return true; } } auto destination = guest->GetDestination() + CoordsDirectionDelta[guest->PeepDirection]; guest->SetDestination(destination, 9); guest->MoveTo({ coords, guest->z }); guest->SetState(PeepState::LeavingPark); guest->Var37 = 0; if (guest->PeepFlags & PEEP_FLAGS_TRACKING) { auto ft = Formatter(); guest->FormatNameTo(ft); if (gConfigNotifications.guest_left_park) { News::AddItemToQueue(News::ItemType::PeepOnRide, STR_PEEP_TRACKING_LEFT_PARK, guest->sprite_index, ft); } } return true; } // Peep is entering the park. if (guest->State != PeepState::EnteringPark) { peep_return_to_centre_of_tile(guest); return true; } if (!(gParkFlags & PARK_FLAGS_PARK_OPEN)) { guest->State = PeepState::LeavingPark; guest->Var37 = 1; decrement_guests_heading_for_park(); peep_window_state_update(guest); peep_return_to_centre_of_tile(guest); return true; } bool found = false; auto entrance = std::find_if( gParkEntrances.begin(), gParkEntrances.end(), [coords](const auto& e) { return coords.ToTileStart() == e; }); if (entrance != gParkEntrances.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 = map_get_first_element_at(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->base_height) { continue; } found = true; break; } if (direction_reverse(slopeDirection) != entranceDirection) continue; if (z - 2 != nextTileElement->base_height) continue; found = true; break; } if (z != nextTileElement->base_height) { continue; } found = true; break; } while (!(nextTileElement++)->IsLastForTile()); } if (!found) { guest->State = PeepState::LeavingPark; guest->Var37 = 1; decrement_guests_heading_for_park(); peep_window_state_update(guest); peep_return_to_centre_of_tile(guest); return true; } money16 entranceFee = park_get_entrance_fee(); 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; decrement_guests_heading_for_park(); peep_window_state_update(guest); peep_return_to_centre_of_tile(guest); return true; } gTotalIncomeFromAdmissions += entranceFee; guest->SpendMoney(guest->PaidToEnter, entranceFee, ExpenditureType::ParkEntranceTickets); guest->PeepFlags |= PEEP_FLAGS_HAS_PAID_FOR_PARK_ENTRY; } gTotalAdmissions++; window_invalidate_by_number(WC_PARK_INFORMATION, 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 peep_footpath_move_forward(Peep* peep, const CoordsXYE& coords, bool vandalism) { auto tile_element = coords.element; peep->NextLoc = { coords.ToTileStart(), tile_element->GetBaseZ() }; peep->SetNextFlags(tile_element->AsPath()->GetSlopeDirection(), tile_element->AsPath()->IsSloped(), false); int16_t z = peep->GetZOnSlope(coords.x, coords.y); auto* guest = peep->As(); 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 ((scenario_rand() & 0xFFFF) <= 10922) { guest->InsertNewThought(PeepThoughtType::Vandalism); guest->HappinessTarget = std::max(0, guest->HappinessTarget - 17); } vandalThoughtTimeout = 3; } } if (vandalThoughtTimeout && (scenario_rand() & 0xFFFF) <= 4369) { vandalThoughtTimeout--; } guest->VandalismSeen = (vandalThoughtTimeout << 6) | vandalisedTiles; uint16_t crowded = 0; uint8_t litter_count = 0; uint8_t sick_count = 0; auto quad = EntityTileList(coords); for (auto entity : quad) { if (auto other_peep = entity->As(); other_peep != nullptr) { if (other_peep->State != PeepState::Walking) continue; if (abs(other_peep->z - guest->NextLoc.z) > 16) continue; crowded++; continue; } if (auto litter = entity->As(); litter != nullptr) { if (abs(litter->z - guest->NextLoc.z) > 16) continue; litter_count++; if (litter->SubType != Litter::Type::Vomit && litter->SubType != Litter::Type::VomitAlt) continue; litter_count--; sick_count++; } } if (crowded >= 10 && guest->State == PeepState::Walking && (scenario_rand() & 0xFFFF) <= 21845) { guest->InsertNewThought(PeepThoughtType::Crowded); guest->HappinessTarget = std::max(0, guest->HappinessTarget - 14); } litter_count = std::min(static_cast(3), litter_count); sick_count = std::min(static_cast(3), sick_count); uint8_t disgusting_time = guest->DisgustingCount & 0xC0; uint8_t disgusting_count = ((guest->DisgustingCount & 0xF) << 2) | sick_count; guest->DisgustingCount = disgusting_count | disgusting_time; if (disgusting_time & 0xC0 && (scenario_rand() & 0xFFFF) <= 4369) { // Reduce the disgusting time guest->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) { guest->InsertNewThought(PeepThoughtType::PathDisgusting); guest->HappinessTarget = std::max(0, guest->HappinessTarget - 17); // Reset disgusting time guest->DisgustingCount |= 0xC0; } } uint8_t litter_time = guest->LitterCount & 0xC0; litter_count = ((guest->LitterCount & 0xF) << 2) | litter_count; guest->LitterCount = litter_count | litter_time; if (litter_time & 0xC0 && (scenario_rand() & 0xFFFF) <= 4369) { // Reduce the litter time guest->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) { 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 peep_interact_with_path(Peep* peep, const CoordsXYE& coords) { // 0x00F1AEE2 auto tile_element = coords.element; 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(); auto* guest = peep->As(); if (map_is_location_owned({ coords, z })) { if (guest != nullptr && guest->OutsideOfPark) { peep_return_to_centre_of_tile(guest); return; } } else { if (guest == nullptr || !guest->OutsideOfPark) { peep_return_to_centre_of_tile(peep); return; } } if (guest != nullptr && tile_element->AsPath()->IsQueue()) { auto rideIndex = tile_element->AsPath()->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) { peep_footpath_move_forward(guest, { coords, tile_element }, vandalism_present); } else { // Queue got disconnected from the original ride. guest->InteractionRideIndex = RideId::GetNull(); guest->RemoveFromQueue(); guest->SetState(PeepState::One); peep_footpath_move_forward(guest, { coords, tile_element }, vandalism_present); } } else { // Peep is not queuing. guest->TimeLost = 0; auto stationNum = tile_element->AsPath()->GetStationIndex(); if ((tile_element->AsPath()->HasQueueBanner()) && (tile_element->AsPath()->GetQueueBannerDirection() == direction_reverse(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 = get_ride(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->sprite_index; guest->GuestNextInQueue = old_last_peep; station.QueueLength++; peep_decrement_num_riders(guest); guest->CurrentRide = rideIndex; guest->CurrentRideStation = stationNum; guest->State = PeepState::Queuing; guest->DaysInQueue = 0; peep_window_state_update(guest); guest->RideSubState = PeepRideSubState::InQueue; guest->DestinationTolerance = 2; guest->TimeInQueue = 0; if (guest->PeepFlags & PEEP_FLAGS_TRACKING) { auto ft = Formatter(); guest->FormatNameTo(ft); ride->FormatNameTo(ft); if (gConfigNotifications.guest_queuing_for_ride) { News::AddItemToQueue( News::ItemType::PeepOnRide, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, guest->sprite_index, ft); } } peep_footpath_move_forward(guest, { coords, tile_element }, vandalism_present); } else { // Peep has decided not to go on the ride. peep_return_to_centre_of_tile(guest); } } else { /* Peep is approaching a queue tile without a ride * sign facing the peep. */ peep_footpath_move_forward(guest, { coords, tile_element }, vandalism_present); } } } else { peep->InteractionRideIndex = RideId::GetNull(); if (guest != nullptr && peep->State == PeepState::Queuing) { guest->RemoveFromQueue(); guest->SetState(PeepState::One); } peep_footpath_move_forward(peep, { coords, tile_element }, vandalism_present); } } /** * * rct2: 0x00693F70 */ static bool peep_interact_with_shop(Peep* peep, const CoordsXYE& coords) { RideId rideIndex = coords.element->AsTrack()->GetRideIndex(); auto ride = get_ride(rideIndex); if (ride == nullptr || !ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP)) return false; auto* guest = peep->As(); if (guest == nullptr) { peep_return_to_centre_of_tile(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) { peep_return_to_centre_of_tile(guest); return true; } if (guest->InteractionRideIndex == rideIndex) { peep_return_to_centre_of_tile(guest); return true; } if (guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK) { peep_return_to_centre_of_tile(guest); return true; } if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_PEEP_SHOULD_GO_INSIDE_FACILITY)) { guest->TimeLost = 0; if (!guest->ShouldGoOnRide(ride, StationIndex::FromUnderlying(0), false, false)) { peep_return_to_centre_of_tile(guest); 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); } auto coordsCentre = coords.ToTileCentre(); guest->SetDestination(coordsCentre, 3); guest->CurrentRide = rideIndex; guest->SetState(PeepState::EnteringRide); guest->RideSubState = PeepRideSubState::ApproachShop; guest->GuestTimeOnRide = 0; ride->cur_num_customers++; if (guest->PeepFlags & PEEP_FLAGS_TRACKING) { auto ft = Formatter(); guest->FormatNameTo(ft); ride->FormatNameTo(ft); rct_string_id string_id = ride->GetRideTypeDescriptor().HasFlag(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::AddItemToQueue(News::ItemType::PeepOnRide, string_id, guest->sprite_index, ft); } } } else { if (guest->GuestHeadingToRideId == rideIndex) guest->GuestHeadingToRideId = RideId::GetNull(); guest->ActionSpriteImageOffset = _unk_F1AEF0; guest->SetState(PeepState::Buying); guest->CurrentRide = rideIndex; guest->SubState = 0; } 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 == PeepActionType::Idle) Action = PeepActionType::Walking; auto* guest = As(); if (State == PeepState::Queuing && guest != nullptr) { if (guest->UpdateQueuePosition(previousAction)) return; } std::optional loc; if (loc = UpdateAction(); !loc.has_value()) { pathing_result |= PATHING_DESTINATION_REACHED; uint8_t result = 0; if (guest != nullptr) { result = guest_path_finding(guest); } else { auto* staff = As(); result = staff->DoPathFinding(); } if (result != 0) return; if (loc = UpdateAction(); !loc.has_value()) 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 (guest != nullptr && guest->OutsideOfPark) { 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() == TileElementType::Path) { peep_interact_with_path(this, { newLoc, tileElement }); tile_result = tileElement; return; } if (tileElement->GetType() == TileElementType::Track) { if (peep_interact_with_shop(this, { newLoc, tileElement })) { tile_result = tileElement; return; } } else if (tileElement->GetType() == TileElementType::Entrance) { if (peep_interact_with_entrance(this, { newLoc, tileElement }, pathing_result)) { tile_result = tileElement; return; } } } while (!(tileElement++)->IsLastForTile()); if (Is() || (GetNextIsSurface())) { int16_t height = abs(tile_element_height(newLoc) - z); if (height <= 3 || (Is() && height <= 32)) { InteractionRideIndex = RideId::GetNull(); if (guest != nullptr && State == PeepState::Queuing) { guest->RemoveFromQueue(); SetState(PeepState::One); } 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; } auto* staff = As(); 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)) { peep_return_to_centre_of_tile(staff); 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); } /** * 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 EntityId sprite_index_a, const EntityId sprite_index_b) { Peep const* peep_a = GetEntity(sprite_index_a); Peep const* peep_b = GetEntity(sprite_index_b); if (peep_a == nullptr || peep_b == nullptr) { return 0; } // Compare types if (peep_a->Type != peep_b->Type) { return static_cast(peep_a->Type) - static_cast(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 char nameA[256]{}; Formatter ft; peep_a->FormatNameTo(ft); format_string(nameA, sizeof(nameA), STR_STRINGID, ft.Data()); char nameB[256]{}; ft.Rewind(); peep_b->FormatNameTo(ft); format_string(nameB, sizeof(nameB), STR_STRINGID, ft.Data()); 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(); } 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->HasItem(ShopItem::Balloon)) { peep->RemoveItem(ShopItem::Balloon); if (peep->SpriteType == PeepSpriteType::Balloon && peep->x != LOCATION_NULL) { Balloon::Create({ peep->x, peep->y, spawn_height }, peep->BalloonColour, false); peep->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY; peep->UpdateSpriteType(); } } } /** * * rct2: 0x0069A512 */ void Peep::RemoveFromRide() { auto* guest = As(); if (guest != nullptr && State == PeepState::Queuing) { guest->RemoveFromQueue(); } StateReset(); } void Peep::SetDestination(const CoordsXY& coords) { DestinationX = static_cast(coords.x); DestinationY = static_cast(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 << SpriteType; 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 << ActionSpriteType; stream << NextActionSpriteType; stream << ActionSpriteImageOffset; stream << Action; stream << ActionFrame; stream << StepProgress; stream << PeepDirection; stream << InteractionRideIndex; stream << Id; stream << PathCheckOptimisation; stream << PathfindGoal; stream << PathfindHistory; stream << WalkingFrameNum; stream << PeepFlags; } void Peep::Paint(paint_session& session, int32_t imageDirection) const { PROFILED_FUNCTION(); #ifdef __ENABLE_LIGHTFX__ if (lightfx_is_available()) { if (Is()) { auto loc = GetLocation(); switch (sprite_direction) { case 0: loc.x -= 10; break; case 8: loc.y += 10; break; case 16: loc.x += 10; break; case 24: loc.y -= 10; break; default: return; } LightfxAdd3DLight(*this, 0, loc, LightType::Spot1); } } #endif rct_drawpixelinfo* dpi = &session.DPI; if (dpi->zoom_level > ZoomLevel{ 2 }) { return; } if (session.ViewFlags & VIEWPORT_FLAG_INVISIBLE_PEEPS) { return; } if ((session.ViewFlags & VIEWPORT_FLAG_INVISIBLE_GUESTS) && Is()) { return; } if ((session.ViewFlags & VIEWPORT_FLAG_INVISIBLE_STAFF) && Is()) { return; } PeepActionSpriteType actionSpriteType = ActionSpriteType; uint8_t imageOffset = ActionSpriteImageOffset; if (Action == PeepActionType::Idle) { actionSpriteType = NextActionSpriteType; imageOffset = 0; } // In the following 4 calls to PaintAddImageAsParent/PaintAddImageAsChild, we add 5 (instead of 3) to the // bound_box_offset_z to make sure peeps are drawn on top of railways uint32_t baseImageId = (imageDirection >> 3) + GetPeepAnimation(SpriteType, actionSpriteType).base_image + imageOffset * 4; uint32_t imageId = baseImageId | TshirtColour << 19 | TrousersColour << 24 | IMAGE_TYPE_REMAP | IMAGE_TYPE_REMAP_2_PLUS; PaintAddImageAsParent(session, imageId, { 0, 0, z }, { 1, 1, 11 }, { 0, 0, z + 5 }); auto* guest = As(); if (guest != nullptr) { if (baseImageId >= 10717 && baseImageId < 10749) { imageId = (baseImageId + 32) | guest->HatColour << 19 | IMAGE_TYPE_REMAP; PaintAddImageAsChild(session, imageId, { 0, 0, z }, { 1, 1, 11 }, { 0, 0, z + 5 }); return; } if (baseImageId >= 10781 && baseImageId < 10813) { imageId = (baseImageId + 32) | guest->BalloonColour << 19 | IMAGE_TYPE_REMAP; PaintAddImageAsChild(session, imageId, { 0, 0, z }, { 1, 1, 11 }, { 0, 0, z + 5 }); return; } if (baseImageId >= 11197 && baseImageId < 11229) { imageId = (baseImageId + 32) | guest->UmbrellaColour << 19 | IMAGE_TYPE_REMAP; PaintAddImageAsChild(session, imageId, { 0, 0, z }, { 1, 1, 11 }, { 0, 0, z + 5 }); return; } } }