/***************************************************************************** * Copyright (c) 2014-2019 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; uint16_t gNumGuestsInPark; uint16_t gNumGuestsInParkLastWeek; uint16_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; // uint32_t gPeepPathFindAltStationNum; 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_give_real_name(Peep* peep); static void peep_release_balloon(Guest* peep, int16_t spawn_height); // clang-format off /** rct2: 0x00981DB0 */ static struct { PeepActionType action; uint8_t flags; } PeepThoughtToActionMap[] = { { PEEP_ACTION_SHAKE_HEAD, 1 }, { PEEP_ACTION_EMPTY_POCKETS, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_WOW, 1 }, { PEEP_ACTION_NONE_2, 2 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 4 }, { PEEP_ACTION_SHAKE_HEAD, 4 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_WAVE, 0 }, { PEEP_ACTION_JOY, 1 }, { PEEP_ACTION_CHECK_TIME, 1 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_WAVE, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_WAVE, 0 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_DISGUST, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_BEING_WATCHED, 0 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 1 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_SHAKE_HEAD, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_NONE_2, 0 }, { PEEP_ACTION_JOY, 0 }, { PEEP_ACTION_NONE_2, 1 }, }; 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() { return peep.sprite_identifier == SPRITE_IDENTIFIER_PEEP; } Peep* rct_sprite::AsPeep() { Peep* result = nullptr; if (IsPeep()) { return (Peep*)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((rct_sprite*)this); } void Peep::MoveTo(int16_t destX, int16_t destY, int16_t destZ) { sprite_move(destX, destY, destZ, (rct_sprite*)this); } 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->generic.sprite_identifier != SPRITE_IDENTIFIER_PEEP) 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; uint16_t spriteIndex; Peep* peep; if (gScreenFlags & SCREEN_FLAGS_EDITOR) return; spriteIndex = gSpriteListHead[SPRITE_LIST_PEEP]; i = 0; while (spriteIndex != SPRITE_INDEX_NULL) { peep = &(get_sprite(spriteIndex)->peep); spriteIndex = peep->next; if ((uint32_t)(i & 0x7F) != (gCurrentTicks & 0x7F)) { peep->Update(); } else { peep_128_tick_update(peep, i); if (peep->linked_list_index == SPRITE_LIST_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(next_x / 32, next_y / 32); uint8_t map_type = TILE_ELEMENT_TYPE_PATH; if (GetNextIsSurface()) { map_type = TILE_ELEMENT_TYPE_SURFACE; } int32_t height = next_z; do { if (tile_element->GetType() == map_type) { if (height == tile_element->base_height) { // 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 LocationXY16 word_981D7C[4] = { { -2, 0 }, { 0, 2 }, { 2, 0 }, { 0, -2 } }; bool Peep::UpdateAction() { int16_t actionX, actionY, xy_distance; return UpdateAction(&actionX, &actionY, &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. */ bool Peep::UpdateAction(int16_t* actionX, int16_t* actionY, int16_t* xy_distance) { _unk_F1AEF0 = action_sprite_image_offset; if (action == PEEP_ACTION_NONE_1) { action = PEEP_ACTION_NONE_2; } *actionX = x - destination_x; *actionY = y - destination_y; int32_t x_delta = abs(*actionX); int32_t y_delta = abs(*actionY); *xy_distance = x_delta + y_delta; if (action == PEEP_ACTION_NONE_1 || action == PEEP_ACTION_NONE_2) { if (*xy_distance <= destination_tolerance) { return false; } int32_t nextDirection = 0; if (x_delta < y_delta) { nextDirection = 8; if (*actionY >= 0) { nextDirection = 24; } } else { nextDirection = 16; if (*actionX >= 0) { nextDirection = 0; } } sprite_direction = nextDirection; *actionX = x + word_981D7C[nextDirection / 8].x; *actionY = y + word_981D7C[nextDirection / 8].y; 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 true; } 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(); Invalidate(); *actionX = x; *actionY = y; return true; } 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) { Invalidate(); *actionX = x; *actionY = y; return true; } // 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); Invalidate(); *actionX = x; *actionY = y; return true; } /** * 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) { Ride* ride = get_ride(peep->current_ride); 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) { Ride* ride = get_ride(peep->current_ride); 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(); } Invalidate(); sprite_move(LOCATION_NULL, y, z, (rct_sprite*)this); SetState(PEEP_STATE_PICKED); sub_state = 0; } void Peep::PickupAbort(int32_t old_x) { if (state != PEEP_STATE_PICKED) return; sprite_move(old_x, y, z + 8, (rct_sprite*)this); Invalidate(); if (x != (int16_t)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(TileCoordsXYZ location, bool apply) { TileElement* tileElement = map_get_path_element_at(location.x, location.y, location.z); if (!tileElement) { tileElement = map_get_surface_element_at(location.x, location.y); } if (!tileElement) return false; CoordsXYZ destination = { location.x * 32, location.y * 32, location.z * 8 }; // Set the coordinate of destination to be exactly // in the middle of a tile. destination.x += 16; destination.y += 16; destination.z = tileElement->base_height * 8 + 16; if (!map_is_location_owned(location.x * 32, location.y * 32, destination.z)) { gGameCommandErrorTitle = STR_ERR_CANT_PLACE_PERSON_HERE; return false; } if (!map_can_construct_at(destination.x, destination.y, destination.z / 8, (destination.z / 8) + 1, { 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) { sprite_move(destination.x, destination.y, destination.z, (rct_sprite*)this); Invalidate(); 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((rct_sprite*)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. int16_t actionX, actionY, xy_distance; UpdateAction(&actionX, &actionY, &xy_distance); if (action == PEEP_ACTION_DROWNING) return; if (gConfigNotifications.guest_died) { set_format_arg(0, rct_string_id, name_string_idx); set_format_arg(2, uint32_t, id); news_item_add_to_queue(NEWS_ITEM_BLANK, STR_NEWS_ITEM_GUEST_DROWNED, actionX | (actionY << 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 / 32, y / 32); 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->base_height * 8; 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() * 16; if (height - 4 >= z && height < z + 20) { // Looks like we are drowning! Invalidate(); 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(); Invalidate(); peep_window_state_update(this); return; } } int32_t map_height = tile_element_height(0xFFFF & x, 0xFFFF & 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) { Invalidate(); if (z <= 1) { // Remove peep if it has gone to the void Remove(); return; } MoveTo(x, y, z - 2); Invalidate(); return; } Invalidate(); MoveTo(x, y, saved_height); Invalidate(); next_x = x & 0xFFE0; next_y = y & 0xFFE0; next_z = saved_map->base_height; 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; uint16_t guests_in_park = gNumGuestsInPark; int32_t hunger_counter = 0, lost_counter = 0, noexit_counter = 0, thirst_counter = 0, litter_counter = 0, disgust_counter = 0, bathroom_counter = 0, vandalism_counter = 0; uint8_t* warning_throttle = gPeepWarningThrottle; gRideCount = ride_get_count(); // refactor this to somewhere else 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_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_type_has_flag(ride->type, RIDE_TYPE_FLAG_SELLS_DRINKS)) thirst_counter++; break; case PEEP_THOUGHT_TYPE_BATHROOM: if (peep->guest_heading_to_ride_id == 0xFF) { bathroom_counter++; break; } ride = get_ride(peep->guest_heading_to_ride_id); if (!ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_IS_BATHROOM)) bathroom_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 >= guests_in_park / 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 >= guests_in_park / 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 (bathroom_counter >= PEEP_BATHROOM_WARNING_THRESHOLD && bathroom_counter >= guests_in_park / 16) { warning_throttle[2] = 4; if (gConfigNotifications.guest_warnings) { news_item_add_to_queue(NEWS_ITEM_PEEPS, STR_PEEPS_CANT_FIND_BATHROOM, 22); } } if (warning_throttle[3]) --warning_throttle[3]; else if (litter_counter >= PEEP_LITTER_WARNING_THRESHOLD && litter_counter >= guests_in_park / 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 >= guests_in_park / 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 >= guests_in_park / 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->view_x > peep->sprite_right) continue; if (viewport->view_x + viewport->view_width < peep->sprite_left) continue; if (viewport->view_y > peep->sprite_bottom) continue; if (viewport->view_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(); peep->Invalidate(); } } // 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->days_in_queue < 255) { peep->days_in_queue += 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(); Invalidate(); } 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->favourite_ride = RIDE_ID_NULL; peep->favourite_ride_rating = 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->sprite_direction = 0; sprite_move(coords.x, coords.y, coords.z, (rct_sprite*)peep); peep->Invalidate(); 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 = (intensityHighest << 4) | intensityLowest; 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_string_idx = STR_GUEST_X; 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 = 0xFF; peep->item_standard_flags = 0; peep->item_extra_flags = 0; peep->guest_heading_to_ride_id = RIDE_ID_NULL; peep->litter_count = 0; peep->disgusting_count = 0; peep->vandalism_seen = 0; peep->paid_to_enter = 0; peep->paid_on_rides = 0; peep->paid_on_food = 0; peep->paid_on_drink = 0; peep->paid_on_souvenirs = 0; peep->no_of_food = 0; peep->no_of_drinks = 0; peep->no_of_souvenirs = 0; peep->surroundings_thought_timeout = 0; peep->angriness = 0; peep->time_lost = 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; if (gParkFlags & PARK_FLAGS_SHOW_REAL_GUEST_NAMES) { peep_give_real_name(peep); } peep_update_name_sort(peep); increment_guests_heading_for_park(); return peep; } void Peep::FormatActionTo(void* argsV) const { auto args = (uint8_t*)argsV; switch (state) { case PEEP_STATE_FALLING: set_format_arg_on(args, 0, rct_string_id, action == PEEP_ACTION_DROWNING ? STR_DROWNING : STR_WALKING); break; case PEEP_STATE_1: set_format_arg_on(args, 0, rct_string_id, 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) { set_format_arg_on( args, 0, rct_string_id, ride_type_has_flag(ride->type, RIDE_TYPE_FLAG_IN_RIDE) ? STR_IN_RIDE : STR_ON_RIDE); ride->FormatNameTo(args + 2); } else { set_format_arg_on(args, 0, rct_string_id, STR_ON_RIDE); set_format_arg_on(args, 2, rct_string_id, STR_NONE); } break; } case PEEP_STATE_BUYING: { set_format_arg_on(args, 0, rct_string_id, STR_AT_RIDE); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(args + 2); } else { set_format_arg_on(args, 2, rct_string_id, 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) { set_format_arg_on(args, 0, rct_string_id, STR_HEADING_FOR); ride->FormatNameTo(args + 2); } } else { set_format_arg_on( args, 0, rct_string_id, (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) { set_format_arg_on(args, 0, rct_string_id, STR_QUEUING_FOR); ride->FormatNameTo(args + 2); } break; } case PEEP_STATE_SITTING: set_format_arg_on(args, 0, rct_string_id, STR_SITTING); break; case PEEP_STATE_WATCHING: if (current_ride != RIDE_ID_NULL) { auto ride = get_ride(current_ride); if (ride != nullptr) { set_format_arg_on( args, 0, rct_string_id, (current_seat & 0x1) ? STR_WATCHING_CONSTRUCTION_OF : STR_WATCHING_RIDE); ride->FormatNameTo(args + 2); } } else { set_format_arg_on( args, 0, rct_string_id, (current_seat & 0x1) ? STR_WATCHING_NEW_RIDE_BEING_CONSTRUCTED : STR_LOOKING_AT_SCENERY); } break; case PEEP_STATE_PICKED: set_format_arg_on(args, 0, rct_string_id, STR_SELECT_LOCATION); break; case PEEP_STATE_PATROLLING: case PEEP_STATE_ENTERING_PARK: case PEEP_STATE_LEAVING_PARK: set_format_arg_on(args, 0, rct_string_id, STR_WALKING); break; case PEEP_STATE_MOWING: set_format_arg_on(args, 0, rct_string_id, STR_MOWING_GRASS); break; case PEEP_STATE_SWEEPING: set_format_arg_on(args, 0, rct_string_id, STR_SWEEPING_FOOTPATH); break; case PEEP_STATE_WATERING: set_format_arg_on(args, 0, rct_string_id, STR_WATERING_GARDENS); break; case PEEP_STATE_EMPTYING_BIN: set_format_arg_on(args, 0, rct_string_id, STR_EMPTYING_LITTER_BIN); break; case PEEP_STATE_ANSWERING: if (sub_state == 0) { set_format_arg_on(args, 0, rct_string_id, STR_WALKING); } else if (sub_state == 1) { set_format_arg_on(args, 0, rct_string_id, STR_ANSWERING_RADIO_CALL); } else { set_format_arg_on(args, 0, rct_string_id, STR_RESPONDING_TO_RIDE_BREAKDOWN_CALL); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(args + 2); } else { set_format_arg_on(args, 2, rct_string_id, STR_NONE); } } break; case PEEP_STATE_FIXING: { set_format_arg_on(args, 0, rct_string_id, STR_FIXING_RIDE); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(args + 2); } else { set_format_arg_on(args, 2, rct_string_id, STR_NONE); } break; } case PEEP_STATE_HEADING_TO_INSPECTION: { set_format_arg_on(args, 0, rct_string_id, STR_HEADING_TO_RIDE_FOR_INSPECTION); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(args + 2); } else { set_format_arg_on(args, 2, rct_string_id, STR_NONE); } break; } case PEEP_STATE_INSPECTING: { set_format_arg_on(args, 0, rct_string_id, STR_INSPECTING_RIDE); auto ride = get_ride(current_ride); if (ride != nullptr) { ride->FormatNameTo(args + 2); } else { set_format_arg_on(args, 2, rct_string_id, STR_NONE); } break; } } } /** * 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) { set_format_arg(0, rct_string_id, PeepThoughts[thought->type]); uint8_t flags = PeepThoughtToActionMap[thought->type].flags; if (flags & 1) { auto ride = get_ride(thought->item); if (ride != nullptr) { ride->FormatNameTo(gCommonFormatArgs + 2); } else { set_format_arg(2, rct_string_id, STR_NONE); } } else if (flags & 2) { set_format_arg(2, rct_string_id, ShopItems[thought->item].Naming.Singular); } else if (flags & 4) { set_format_arg(2, rct_string_id, 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) { if (peep->type == PEEP_TYPE_GUEST) { set_map_tooltip_format_arg( 0, rct_string_id, (peep->peep_flags & PEEP_FLAGS_TRACKING) ? STR_TRACKED_GUEST_MAP_TIP : STR_GUEST_MAP_TIP); set_map_tooltip_format_arg(2, uint32_t, get_peep_face_sprite_small(peep)); set_map_tooltip_format_arg(6, rct_string_id, peep->name_string_idx); set_map_tooltip_format_arg(8, uint32_t, peep->id); peep->FormatActionTo(gMapTooltipFormatArgs + 12); } else { set_map_tooltip_format_arg(0, rct_string_id, STR_STAFF_MAP_TIP); set_map_tooltip_format_arg(2, rct_string_id, peep->name_string_idx); set_map_tooltip_format_arg(4, uint32_t, peep->id); peep->FormatActionTo(gMapTooltipFormatArgs + 8); } } /** * 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; } } int16_t xy_dist, x, y; if (peep->action < PEEP_ACTION_NONE_1) peep->UpdateAction(&x, &y, &xy_dist); 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(); ride_id_t rideIndex = tile_element->AsEntrance()->GetRideIndex(); auto ride = get_ride(rideIndex); // 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 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->time_lost = 0; uint8_t 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->days_in_queue = 0; peep->SetState(PEEP_STATE_QUEUING); peep->sub_state = 11; peep->time_in_queue = 0; if (peep->peep_flags & PEEP_FLAGS_TRACKING) { set_format_arg(0, rct_string_id, peep->name_string_idx); set_format_arg(2, uint32_t, peep->id); ride->FormatNameTo(gCommonFormatArgs + 6); 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->Invalidate(); sprite_move(x, y, peep->z, (rct_sprite*)peep); peep->Invalidate(); peep->SetState(PEEP_STATE_LEAVING_PARK); peep->var_37 = 0; if (peep->peep_flags & PEEP_FLAGS_TRACKING) { set_format_arg(0, rct_string_id, peep->name_string_idx); set_format_arg(2, uint32_t, peep->id); 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 / 32, next_y / 32); do { 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->item_standard_flags & PEEP_ITEM_VOUCHER) { if (peep->voucher_type == VOUCHER_TYPE_PARK_ENTRY_HALF_PRICE) { entranceFee /= 2; peep->item_standard_flags &= ~PEEP_ITEM_VOUCHER; peep->window_invalidate_flags |= PEEP_INVALIDATE_PEEP_INVENTORY; } else if (peep->voucher_type == VOUCHER_TYPE_PARK_ENTRY_FREE) { entranceFee = 0; peep->item_standard_flags &= ~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; gCommandExpenditureType = RCT_EXPENDITURE_TYPE_PARK_ENTRANCE_TICKETS; guest->SpendMoney(peep->paid_to_enter, entranceFee); 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->Invalidate(); sprite_move(x, y, peep->z, (rct_sprite*)peep); peep->Invalidate(); } } /** * * rct2: 0x006946D8 */ static void peep_footpath_move_forward(Peep* peep, int16_t x, int16_t y, TileElement* tile_element, bool vandalism) { peep->next_x = (x & 0xFFE0); peep->next_y = (y & 0xFFE0); peep->next_z = tile_element->base_height; 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->Invalidate(); sprite_move(x, y, z, (rct_sprite*)peep); peep->Invalidate(); return; } uint8_t vandalThoughtTimeout = (peep->vandalism_seen & 0xC0) >> 6; // Advance the vandalised tiles by 1 uint8_t vandalisedTiles = (peep->vandalism_seen * 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->vandalism_seen = (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->generic.sprite_identifier == SPRITE_IDENTIFIER_PEEP) { Peep* other_peep = (Peep*)sprite; if (other_peep->state != PEEP_STATE_WALKING) continue; if (abs(other_peep->z - peep->next_z * 8) > 16) continue; crowded++; continue; } else if (sprite->generic.sprite_identifier == SPRITE_IDENTIFIER_LITTER) { rct_litter* litter = (rct_litter*)sprite; if (abs(litter->z - peep->next_z * 8) > 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->disgusting_count & 0xC0; uint8_t disgusting_count = ((peep->disgusting_count & 0xF) << 2) | sick_count; peep->disgusting_count = disgusting_count | disgusting_time; if (disgusting_time & 0xC0 && (scenario_rand() & 0xFFFF) <= 4369) { // Reduce the disgusting time peep->disgusting_count -= 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->disgusting_count |= 0xC0; } } uint8_t litter_time = peep->litter_count & 0xC0; litter_count = ((peep->litter_count & 0xF) << 2) | litter_count; peep->litter_count = litter_count | litter_time; if (litter_time & 0xC0 && (scenario_rand() & 0xFFFF) <= 4369) { // Reduce the litter time peep->litter_count -= 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->litter_count |= 0xC0; } } peep->Invalidate(); sprite_move(x, y, z, (rct_sprite*)peep); peep->Invalidate(); } /** * * 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->base_height * 8; 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->time_lost = 0; uint8_t 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->days_in_queue = 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) { set_format_arg(0, rct_string_id, peep->name_string_idx); set_format_arg(2, uint32_t, peep->id); ride->FormatNameTo(gCommonFormatArgs + 6); 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(); Ride* ride = get_ride(rideIndex); if (!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->time_lost = 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->time_lost = 0; if (!guest->ShouldGoOnRide(ride, 0, false, false)) { peep_return_to_centre_of_tile(peep); return true; } money16 cost = ride->price; if (cost != 0 && !(gParkFlags & PARK_FLAGS_NO_MONEY)) { ride->total_profit += cost; ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME; gCommandExpenditureType = RCT_EXPENDITURE_TYPE_PARK_RIDE_TICKETS; // TODO: Refactor? SpendMoney previously accepted nullptr to not track money, passing a temporary variable as a // workaround money16 money = 0; guest->SpendMoney(money, cost); } 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->time_on_ride = 0; ride->cur_num_customers++; if (peep->peep_flags & PEEP_FLAGS_TRACKING) { set_format_arg(0, rct_string_id, peep->name_string_idx); set_format_arg(2, uint32_t, peep->id); ride->FormatNameTo(gCommonFormatArgs + 6); 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; } int16_t actionX, actionY, xy_dist; if (!UpdateAction(&actionX, &actionY, &xy_dist)) { 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_path_finding(staff); } if (result != 0) return; if (!UpdateAction(&actionX, &actionY, &xy_dist)) return; } if ((actionX & 0xFFE0) == next_x && (actionY & 0xFFE0) == next_y) { int16_t height = GetZOnSlope(actionX, actionY); Invalidate(); MoveTo(actionX, actionY, height); Invalidate(); return; } if (actionX < 32 || actionY < 32 || actionX >= gMapSizeUnits || actionY >= gMapSizeUnits) { 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(actionX / 32, actionY / 32); 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, actionX, actionY, tileElement); tile_result = tileElement; return; } else if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK) { if (peep_interact_with_shop(this, actionX, actionY, tileElement)) { tile_result = tileElement; return; } } else if (tileElement->GetType() == TILE_ELEMENT_TYPE_ENTRANCE) { peep_interact_with_entrance(this, actionX, actionY, tileElement, pathing_result); tile_result = tileElement; return; } } while (!(tileElement++)->IsLastForTile()); if (type == PEEP_TYPE_STAFF || (GetNextIsSurface())) { int16_t height = abs(tile_element_height(actionX, actionY) - 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({ actionX & 0xFFE0, actionY & 0xFFE0 })) { peep_return_to_centre_of_tile(this); return; } tileElement = map_get_surface_element_at({ actionX, actionY }); if (tileElement == nullptr) { peep_return_to_centre_of_tile(this); return; } int16_t water_height = tileElement->AsSurface()->GetWaterHeight(); if (water_height) { 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->staff_mowing_timeout >= 12)) { peep_return_to_centre_of_tile(this); return; } } // The peep is on a surface and not on a path next_x = actionX & 0xFFE0; next_y = actionY & 0xFFE0; next_z = tileElement->base_height; SetNextFlags(0, false, true); height = GetZOnSlope(actionX, actionY); Invalidate(); MoveTo(actionX, actionY, height); Invalidate(); 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 = 0xFF; } /** * 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); } int32_t height = next_z * 8; uint8_t slope = GetNextDirection(); return height + map_height_from_slope({ tile_x, tile_y }, slope, GetNextIsSloped()); } /** * * rct2: 0x0069C483 */ static void peep_give_real_name(Peep* peep) { // Generate a name_string_idx from the peep id using bit twiddling uint16_t ax = (uint16_t)(peep->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; peep->name_string_idx = dx; } static int32_t peep_compare(const void* sprite_index_a, const void* sprite_index_b) { Peep const* peep_a = GET_PEEP(*(uint16_t*)sprite_index_a); Peep const* peep_b = GET_PEEP(*(uint16_t*)sprite_index_b); // Compare types if (peep_a->type != peep_b->type) { return peep_a->type - peep_b->type; } // Simple ID comparison for when both peeps use a number or a generated name const bool both_numbers = (peep_a->name_string_idx >= 767 && peep_a->name_string_idx <= 771 && peep_b->name_string_idx >= 767 && peep_b->name_string_idx <= 771); if (both_numbers) { return peep_a->id - peep_b->id; } const bool both_have_generated_names = (peep_a->name_string_idx >= REAL_NAME_START && peep_a->name_string_idx <= REAL_NAME_END && peep_b->name_string_idx >= REAL_NAME_START && peep_b->name_string_idx <= REAL_NAME_END); if (both_have_generated_names) { rct_string_id peep_a_format = peep_a->name_string_idx + REAL_NAME_START; rct_string_id peep_b_format = peep_b->name_string_idx + REAL_NAME_START; uint16_t peep_a_name = (peep_a_format % std::size(real_names)); uint16_t peep_b_name = (peep_b_format % std::size(real_names)); if (peep_a_name == peep_b_name) { uint16_t peep_a_initial = ((peep_a_format >> 10) % std::size(real_name_initials)); uint16_t peep_b_initial = ((peep_b_format >> 10) % std::size(real_name_initials)); return peep_a_initial - peep_b_initial; } else { return peep_a_name - peep_b_name; } } // At least one of them has a custom name assigned // Compare their names as strings utf8 name_a[256]; utf8 name_b[256]; uint32_t peepIndex = peep_a->id; format_string(name_a, 256, peep_a->name_string_idx, &peepIndex); peepIndex = peep_b->id; format_string(name_b, 256, peep_b->name_string_idx, &peepIndex); return strlogicalcmp(name_a, name_b); } /** * * rct2: 0x00699115 */ void peep_update_name_sort(Peep* peep) { // Remove peep from sprite list uint16_t nextSpriteIndex = peep->next; uint16_t prevSpriteIndex = peep->previous; if (prevSpriteIndex != SPRITE_INDEX_NULL) { Peep* prevPeep = GET_PEEP(prevSpriteIndex); prevPeep->next = nextSpriteIndex; } else { gSpriteListHead[SPRITE_LIST_PEEP] = nextSpriteIndex; } if (nextSpriteIndex != SPRITE_INDEX_NULL) { Peep* nextPeep = GET_PEEP(nextSpriteIndex); nextPeep->previous = prevSpriteIndex; } Peep* otherPeep; uint16_t spriteIndex; FOR_ALL_PEEPS (spriteIndex, otherPeep) { // Check if peep should go before this one if (peep_compare(&peep->sprite_index, &otherPeep->sprite_index) >= 0) { continue; } // Place peep before this one peep->previous = otherPeep->previous; otherPeep->previous = peep->sprite_index; if (peep->previous != SPRITE_INDEX_NULL) { Peep* prevPeep = GET_PEEP(peep->previous); peep->next = prevPeep->next; prevPeep->next = peep->sprite_index; } else { peep->next = gSpriteListHead[SPRITE_LIST_PEEP]; gSpriteListHead[SPRITE_LIST_PEEP] = peep->sprite_index; } goto finish_peep_sort; } // Place peep at the end FOR_ALL_PEEPS (spriteIndex, otherPeep) { if (otherPeep->next == SPRITE_INDEX_NULL) { otherPeep->next = peep->sprite_index; peep->previous = otherPeep->sprite_index; peep->next = SPRITE_INDEX_NULL; goto finish_peep_sort; } } gSpriteListHead[SPRITE_LIST_PEEP] = peep->sprite_index; peep->next = SPRITE_INDEX_NULL; peep->previous = SPRITE_INDEX_NULL; finish_peep_sort: // This is required at the moment because this function reorders peeps in the sprite list sprite_position_tween_reset(); } void peep_sort() { // Count number of peeps uint16_t sprite_index, num_peeps = 0; Peep* peep; FOR_ALL_PEEPS (sprite_index, peep) { num_peeps++; } // No need to sort if (num_peeps < 2) return; // Create a copy of the peep list and sort it using peep_compare uint16_t* peep_list = (uint16_t*)malloc(num_peeps * sizeof(uint16_t)); int32_t i = 0; FOR_ALL_PEEPS (sprite_index, peep) { peep_list[i++] = peep->sprite_index; } qsort(peep_list, num_peeps, sizeof(uint16_t), peep_compare); // Set the correct peep->next and peep->previous using the sorted list for (i = 0; i < num_peeps; i++) { peep = GET_PEEP(peep_list[i]); peep->previous = (i > 0) ? peep_list[i - 1] : SPRITE_INDEX_NULL; peep->next = (i + 1 < num_peeps) ? peep_list[i + 1] : SPRITE_INDEX_NULL; } // Make sure the first peep is set gSpriteListHead[SPRITE_LIST_PEEP] = peep_list[0]; free(peep_list); i = 0; FOR_ALL_PEEPS (sprite_index, peep) { i++; } assert(i == num_peeps); } /** * * rct2: 0x0069926C */ void peep_update_names(bool realNames) { if (realNames) { gParkFlags |= PARK_FLAGS_SHOW_REAL_GUEST_NAMES; Peep* peep; uint16_t spriteIndex; FOR_ALL_GUESTS (spriteIndex, peep) { if (peep->name_string_idx == STR_GUEST_X) { peep_give_real_name(peep); } } } else { gParkFlags &= ~PARK_FLAGS_SHOW_REAL_GUEST_NAMES; Peep* peep; uint16_t spriteIndex; FOR_ALL_GUESTS (spriteIndex, peep) { if (peep->name_string_idx >= REAL_NAME_START && peep->name_string_idx <= REAL_NAME_END) { peep->name_string_idx = STR_GUEST_X; } } } peep_sort(); 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 < UINT16_MAX) { gNumGuestsInPark++; } else { openrct2_assert(false, "Attempt to increment guests in park above max value (65535)."); } } void increment_guests_heading_for_park() { if (gNumGuestsHeadingForPark < UINT16_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->item_standard_flags & PEEP_ITEM_BALLOON) { peep->item_standard_flags &= ~PEEP_ITEM_BALLOON; if (peep->sprite_type == PEEP_SPRITE_TYPE_BALLOON && peep->x != LOCATION_NULL) { create_balloon(peep->x, peep->y, spawn_height, peep->balloon_colour, false); peep->window_invalidate_flags |= PEEP_INVALIDATE_PEEP_INVENTORY; peep->UpdateSpriteType(); } } } /** * * rct2: 0x006966A9 */ void Peep::RemoveFromQueue() { Ride* ride = get_ride(current_ride); 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(); }