/***************************************************************************** * 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 "Sprite.h" #include "../Game.h" #include "../core/ChecksumStream.h" #include "../core/Crypt.h" #include "../core/DataSerialiser.h" #include "../core/Guard.hpp" #include "../core/MemoryStream.h" #include "../interface/Viewport.h" #include "../peep/Peep.h" #include "../peep/RideUseSystem.h" #include "../ride/Vehicle.h" #include "../scenario/Scenario.h" #include "Balloon.h" #include "Duck.h" #include "EntityTweener.h" #include "Fountain.h" #include "MoneyEffect.h" #include "Particle.h" #include #include #include #include #include static rct_sprite _spriteList[MAX_ENTITIES]; static std::array, EnumValue(EntityType::Count)> gEntityLists; static std::vector _freeIdList; static bool _spriteFlashingList[MAX_ENTITIES]; constexpr const uint32_t SPATIAL_INDEX_SIZE = (MAXIMUM_MAP_SIZE_TECHNICAL * MAXIMUM_MAP_SIZE_TECHNICAL) + 1; constexpr const uint32_t SPATIAL_INDEX_LOCATION_NULL = SPATIAL_INDEX_SIZE - 1; static std::array, SPATIAL_INDEX_SIZE> gSpriteSpatialIndex; static void FreeEntity(EntityBase& entity); constexpr size_t GetSpatialIndexOffset(int32_t x, int32_t y) { if (x == LOCATION_NULL) return SPATIAL_INDEX_LOCATION_NULL; const auto tileX = std::clamp(x / COORDS_XY_STEP, 0, MAXIMUM_MAP_SIZE_TECHNICAL); const auto tileY = std::clamp(y / COORDS_XY_STEP, 0, MAXIMUM_MAP_SIZE_TECHNICAL); const auto index = tileX * MAXIMUM_MAP_SIZE_TECHNICAL + tileY; if (index >= std::size(gSpriteSpatialIndex)) { return SPATIAL_INDEX_LOCATION_NULL; } return index; } // Required for GetEntity to return a default template<> bool EntityBase::Is() const { return true; } template<> bool EntityBase::Is() const { return Type == EntityType::Litter; } constexpr bool EntityTypeIsMiscEntity(const EntityType type) { switch (type) { case EntityType::SteamParticle: case EntityType::MoneyEffect: case EntityType::CrashedVehicleParticle: case EntityType::ExplosionCloud: case EntityType::CrashSplash: case EntityType::ExplosionFlare: case EntityType::JumpingFountain: case EntityType::Balloon: case EntityType::Duck: return true; default: return false; } } template<> bool EntityBase::Is() const { return EntityTypeIsMiscEntity(Type); } template<> bool EntityBase::Is() const { return Type == EntityType::SteamParticle; } template<> bool EntityBase::Is() const { return Type == EntityType::ExplosionFlare; } template<> bool EntityBase::Is() const { return Type == EntityType::ExplosionCloud; } uint16_t GetEntityListCount(EntityType type) { return static_cast(gEntityLists[EnumValue(type)].size()); } uint16_t GetNumFreeEntities() { return static_cast(_freeIdList.size()); } std::string rct_sprite_checksum::ToString() const { std::string result; result.reserve(raw.size() * 2); for (auto b : raw) { char buf[3]; snprintf(buf, 3, "%02x", static_cast(b)); result.append(buf); } return result; } EntityBase* try_get_sprite(size_t spriteIndex) { return spriteIndex >= MAX_ENTITIES ? nullptr : &_spriteList[spriteIndex].base; } EntityBase* get_sprite(size_t spriteIndex) { if (spriteIndex == SPRITE_INDEX_NULL) { return nullptr; } openrct2_assert(spriteIndex < MAX_ENTITIES, "Tried getting sprite %u", spriteIndex); return try_get_sprite(spriteIndex); } const std::vector& GetEntityTileList(const CoordsXY& spritePos) { return gSpriteSpatialIndex[GetSpatialIndexOffset(spritePos.x, spritePos.y)]; } void EntityBase::Invalidate() { if (x == LOCATION_NULL) return; int32_t maxZoom = 0; switch (Type) { case EntityType::Vehicle: case EntityType::Guest: case EntityType::Staff: maxZoom = 2; break; case EntityType::CrashedVehicleParticle: case EntityType::JumpingFountain: maxZoom = 0; break; case EntityType::Duck: maxZoom = 1; break; case EntityType::SteamParticle: case EntityType::MoneyEffect: case EntityType::ExplosionCloud: case EntityType::CrashSplash: case EntityType::ExplosionFlare: case EntityType::Balloon: maxZoom = 2; break; case EntityType::Litter: maxZoom = 0; break; default: break; } viewports_invalidate(SpriteRect.GetLeft(), SpriteRect.GetTop(), SpriteRect.GetRight(), SpriteRect.GetBottom(), maxZoom); } static void ResetEntityLists() { for (auto& list : gEntityLists) { list.clear(); } } static void ResetFreeIds() { _freeIdList.clear(); _freeIdList.resize(MAX_ENTITIES); // List needs to be back to front to simplify removing std::iota(std::rbegin(_freeIdList), std::rend(_freeIdList), 0); } const std::list& GetEntityList(const EntityType id) { return gEntityLists[EnumValue(id)]; } /** * * rct2: 0x0069EB13 */ void reset_sprite_list() { gSavedAge = 0; // Free all associated Entity pointers prior to zeroing memory for (int32_t i = 0; i < MAX_ENTITIES; ++i) { auto* spr = GetEntity(i); if (spr == nullptr) { continue; } FreeEntity(*spr); } std::fill(std::begin(_spriteList), std::end(_spriteList), rct_sprite()); OpenRCT2::RideUse::GetHistory().Clear(); OpenRCT2::RideUse::GetTypeHistory().Clear(); for (int32_t i = 0; i < MAX_ENTITIES; ++i) { auto* spr = GetEntity(i); if (spr == nullptr) { continue; } spr->Type = EntityType::Null; spr->sprite_index = i; _spriteFlashingList[i] = false; } ResetEntityLists(); ResetFreeIds(); reset_sprite_spatial_index(); } static void SpriteSpatialInsert(EntityBase* sprite, const CoordsXY& newLoc); /** * * rct2: 0x0069EBE4 * This function looks as though it sets some sort of order for sprites. * Sprites can share their position if this is the case. */ void reset_sprite_spatial_index() { for (auto& vec : gSpriteSpatialIndex) { vec.clear(); } for (size_t i = 0; i < MAX_ENTITIES; i++) { auto* spr = GetEntity(i); if (spr != nullptr && spr->Type != EntityType::Null) { SpriteSpatialInsert(spr, { spr->x, spr->y }); } } } #ifndef DISABLE_NETWORK template void NetworkSerialseEntityType(DataSerialiser& ds) { for (auto* ent : EntityList()) { ent->Serialise(ds); } } template void NetworkSerialiseEntityTypes(DataSerialiser& ds) { (NetworkSerialseEntityType(ds), ...); } rct_sprite_checksum sprite_checksum() { rct_sprite_checksum checksum{}; OpenRCT2::ChecksumStream ms(checksum.raw); DataSerialiser ds(true, ms); NetworkSerialiseEntityTypes(ds); return checksum; } #else rct_sprite_checksum sprite_checksum() { return rct_sprite_checksum{}; } #endif // DISABLE_NETWORK static void sprite_reset(EntityBase* sprite) { // Need to retain how the sprite is linked in lists uint16_t sprite_index = sprite->sprite_index; _spriteFlashingList[sprite_index] = false; rct_sprite* spr = reinterpret_cast(sprite); *spr = rct_sprite(); sprite->sprite_index = sprite_index; sprite->Type = EntityType::Null; } static constexpr uint16_t MAX_MISC_SPRITES = 300; static void AddToEntityList(EntityBase* entity) { auto& list = gEntityLists[EnumValue(entity->Type)]; // Entity list must be in sprite_index order to prevent desync issues list.insert(std::lower_bound(std::begin(list), std::end(list), entity->sprite_index), entity->sprite_index); } static void AddToFreeList(uint16_t index) { // Free list must be in reverse sprite_index order to prevent desync issues _freeIdList.insert(std::upper_bound(std::rbegin(_freeIdList), std::rend(_freeIdList), index).base(), index); } static void RemoveFromEntityList(EntityBase* entity) { auto& list = gEntityLists[EnumValue(entity->Type)]; auto ptr = std::lower_bound(std::begin(list), std::end(list), entity->sprite_index); if (ptr != std::end(list) && *ptr == entity->sprite_index) { list.erase(ptr); } } uint16_t GetMiscEntityCount() { uint16_t count = 0; for (auto id : { EntityType::SteamParticle, EntityType::MoneyEffect, EntityType::CrashedVehicleParticle, EntityType::ExplosionCloud, EntityType::CrashSplash, EntityType::ExplosionFlare, EntityType::JumpingFountain, EntityType::Balloon, EntityType::Duck }) { count += GetEntityListCount(id); } return count; } static void PrepareNewEntity(EntityBase* base, const EntityType type) { // Need to reset all sprite data, as the uninitialised values // may contain garbage and cause a desync later on. sprite_reset(base); base->Type = type; AddToEntityList(base); base->x = LOCATION_NULL; base->y = LOCATION_NULL; base->z = 0; base->sprite_width = 0x10; base->sprite_height_negative = 0x14; base->sprite_height_positive = 0x8; base->SpriteRect = {}; SpriteSpatialInsert(base, { LOCATION_NULL, 0 }); } EntityBase* CreateEntity(EntityType type) { if (_freeIdList.size() == 0) { // No free sprites. return nullptr; } if (EntityTypeIsMiscEntity(type)) { // Misc sprites are commonly used for effects, if there are less than MAX_MISC_SPRITES // free it will fail to keep slots for more relevant sprites. // Also there can't be more than MAX_MISC_SPRITES sprites in this list. uint16_t miscSlotsRemaining = MAX_MISC_SPRITES - GetMiscEntityCount(); if (miscSlotsRemaining >= _freeIdList.size()) { return nullptr; } } auto* entity = GetEntity(_freeIdList.back()); if (entity == nullptr) { return nullptr; } _freeIdList.pop_back(); PrepareNewEntity(entity, type); return entity; } EntityBase* CreateEntityAt(const uint16_t index, const EntityType type) { auto id = std::lower_bound(std::rbegin(_freeIdList), std::rend(_freeIdList), index); if (id == std::rend(_freeIdList) || *id != index) { return nullptr; } auto* entity = GetEntity(index); if (entity == nullptr) { return nullptr; } _freeIdList.erase(std::next(id).base()); PrepareNewEntity(entity, type); return entity; } template void MiscUpdateAllType() { for (auto misc : EntityList()) { misc->Update(); } } template void MiscUpdateAllTypes() { (MiscUpdateAllType(), ...); } /** * * rct2: 0x00672AA4 */ void sprite_misc_update_all() { MiscUpdateAllTypes< SteamParticle, MoneyEffect, VehicleCrashParticle, ExplosionCloud, CrashSplashParticle, ExplosionFlare, JumpingFountain, Balloon, Duck>(); } // Performs a search to ensure that insert keeps next_in_quadrant in sprite_index order static void SpriteSpatialInsert(EntityBase* sprite, const CoordsXY& newLoc) { size_t newIndex = GetSpatialIndexOffset(newLoc.x, newLoc.y); auto& spatialVector = gSpriteSpatialIndex[newIndex]; auto index = std::lower_bound(std::begin(spatialVector), std::end(spatialVector), sprite->sprite_index); spatialVector.insert(index, sprite->sprite_index); } static void SpriteSpatialRemove(EntityBase* sprite) { size_t currentIndex = GetSpatialIndexOffset(sprite->x, sprite->y); auto& spatialVector = gSpriteSpatialIndex[currentIndex]; auto index = std::lower_bound(std::begin(spatialVector), std::end(spatialVector), sprite->sprite_index); if (index != std::end(spatialVector) && *index == sprite->sprite_index) { spatialVector.erase(index, index + 1); } else { log_warning("Bad sprite spatial index. Rebuilding the spatial index..."); reset_sprite_spatial_index(); } } static void SpriteSpatialMove(EntityBase* sprite, const CoordsXY& newLoc) { size_t newIndex = GetSpatialIndexOffset(newLoc.x, newLoc.y); size_t currentIndex = GetSpatialIndexOffset(sprite->x, sprite->y); if (newIndex == currentIndex) return; SpriteSpatialRemove(sprite); SpriteSpatialInsert(sprite, newLoc); } void EntityBase::MoveTo(const CoordsXYZ& newLocation) { if (x != LOCATION_NULL) { // Invalidate old position. Invalidate(); } auto loc = newLocation; if (!map_is_location_valid(loc)) { loc.x = LOCATION_NULL; } SpriteSpatialMove(this, loc); if (loc.x == LOCATION_NULL) { x = loc.x; y = loc.y; z = loc.z; } else { sprite_set_coordinates(loc, this); Invalidate(); // Invalidate new position. } } CoordsXYZ EntityBase::GetLocation() const { return { x, y, z }; } void EntityBase::SetLocation(const CoordsXYZ& newLocation) { x = static_cast(newLocation.x); y = static_cast(newLocation.y); z = static_cast(newLocation.z); } void sprite_set_coordinates(const CoordsXYZ& spritePos, EntityBase* sprite) { auto screenCoords = translate_3d_to_2d_with_z(get_current_rotation(), spritePos); sprite->SpriteRect = ScreenRect( screenCoords - ScreenCoordsXY{ sprite->sprite_width, sprite->sprite_height_negative }, screenCoords + ScreenCoordsXY{ sprite->sprite_width, sprite->sprite_height_positive }); sprite->x = spritePos.x; sprite->y = spritePos.y; sprite->z = spritePos.z; } /** * Frees any dynamically attached memory to the entity, such as peep name. */ static void FreeEntity(EntityBase& entity) { auto* guest = entity.As(); auto* staff = entity.As(); if (staff != nullptr) { staff->SetName({}); staff->ClearPatrolArea(); } else if (guest != nullptr) { guest->SetName({}); OpenRCT2::RideUse::GetHistory().RemoveHandle(guest->sprite_index); OpenRCT2::RideUse::GetTypeHistory().RemoveHandle(guest->sprite_index); } } /** * * rct2: 0x0069EDB6 */ void sprite_remove(EntityBase* sprite) { FreeEntity(*sprite); EntityTweener::Get().RemoveEntity(sprite); RemoveFromEntityList(sprite); // remove from existing list AddToFreeList(sprite->sprite_index); SpriteSpatialRemove(sprite); sprite_reset(sprite); } /** * Loops through all sprites, finds floating objects and removes them. * Returns the amount of removed objects as feedback. */ uint16_t remove_floating_sprites() { uint16_t removed = 0; for (auto* balloon : EntityList()) { sprite_remove(balloon); removed++; } for (auto* duck : EntityList()) { if (duck->IsFlying()) { sprite_remove(duck); removed++; } } for (auto* money : EntityList()) { sprite_remove(money); removed++; } return removed; } void sprite_set_flashing(EntityBase* sprite, bool flashing) { assert(sprite->sprite_index < MAX_ENTITIES); _spriteFlashingList[sprite->sprite_index] = flashing; } bool sprite_get_flashing(EntityBase* sprite) { assert(sprite->sprite_index < MAX_ENTITIES); return _spriteFlashingList[sprite->sprite_index]; }