1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-27 00:34:46 +01:00
Files
OpenRCT2/src/openrct2/world/Sprite.cpp

1194 lines
34 KiB
C++

/*****************************************************************************
* 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 "../Cheats.h"
#include "../Game.h"
#include "../OpenRCT2.h"
#include "../audio/audio.h"
#include "../core/Crypt.h"
#include "../core/Guard.hpp"
#include "../interface/Viewport.h"
#include "../localisation/Date.h"
#include "../localisation/Localisation.h"
#include "../scenario/Scenario.h"
#include "Fountain.h"
#include <algorithm>
#include <cmath>
#include <iterator>
uint16_t gSpriteListHead[static_cast<uint8_t>(EntityListId::Count)];
uint16_t gSpriteListCount[static_cast<uint8_t>(EntityListId::Count)];
static rct_sprite _spriteList[MAX_SPRITES];
static bool _spriteFlashingList[MAX_SPRITES];
uint16_t gSpriteSpatialIndex[SPATIAL_INDEX_SIZE];
const rct_string_id litterNames[12] = { STR_LITTER_VOMIT,
STR_LITTER_VOMIT,
STR_SHOP_ITEM_SINGULAR_EMPTY_CAN,
STR_SHOP_ITEM_SINGULAR_RUBBISH,
STR_SHOP_ITEM_SINGULAR_EMPTY_BURGER_BOX,
STR_SHOP_ITEM_SINGULAR_EMPTY_CUP,
STR_SHOP_ITEM_SINGULAR_EMPTY_BOX,
STR_SHOP_ITEM_SINGULAR_EMPTY_BOTTLE,
STR_SHOP_ITEM_SINGULAR_EMPTY_BOWL_RED,
STR_SHOP_ITEM_SINGULAR_EMPTY_DRINK_CARTON,
STR_SHOP_ITEM_SINGULAR_EMPTY_JUICE_CUP,
STR_SHOP_ITEM_SINGULAR_EMPTY_BOWL_BLUE };
static CoordsXYZ _spritelocations1[MAX_SPRITES];
static CoordsXYZ _spritelocations2[MAX_SPRITES];
static size_t GetSpatialIndexOffset(int32_t x, int32_t y);
static void move_sprite_to_list(SpriteBase* sprite, EntityListId newListIndex);
// Required for GetEntity to return a default
template<> bool SpriteBase::Is<SpriteBase>() const
{
return true;
}
template<> bool SpriteBase::Is<Litter>() const
{
return sprite_identifier == SpriteIdentifier::Litter;
}
template<> bool SpriteBase::Is<SteamParticle>() const
{
return sprite_identifier == SpriteIdentifier::Misc && static_cast<MiscEntityType>(type) == MiscEntityType::SteamParticle;
}
template<> bool SpriteBase::Is<ExplosionFlare>() const
{
return sprite_identifier == SpriteIdentifier::Misc && static_cast<MiscEntityType>(type) == MiscEntityType::ExplosionFlare;
}
template<> bool SpriteBase::Is<ExplosionCloud>() const
{
return sprite_identifier == SpriteIdentifier::Misc && static_cast<MiscEntityType>(type) == MiscEntityType::ExplosionCloud;
}
uint16_t GetEntityListCount(EntityListId list)
{
return gSpriteListCount[static_cast<uint8_t>(list)];
}
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", b);
result.append(buf);
}
return result;
}
SpriteBase* try_get_sprite(size_t spriteIndex)
{
return spriteIndex >= MAX_SPRITES ? nullptr : &_spriteList[spriteIndex].generic;
}
SpriteBase* get_sprite(size_t spriteIndex)
{
if (spriteIndex == SPRITE_INDEX_NULL)
{
return nullptr;
}
openrct2_assert(spriteIndex < MAX_SPRITES, "Tried getting sprite %u", spriteIndex);
return try_get_sprite(spriteIndex);
}
uint16_t sprite_get_first_in_quadrant(const CoordsXY& spritePos)
{
return gSpriteSpatialIndex[GetSpatialIndexOffset(spritePos.x, spritePos.y)];
}
static void invalidate_sprite_max_zoom(SpriteBase* sprite, int32_t maxZoom)
{
if (sprite->sprite_left == LOCATION_NULL)
return;
for (int32_t i = 0; i < MAX_VIEWPORT_COUNT; i++)
{
rct_viewport* viewport = &g_viewport_list[i];
if (viewport->width != 0 && viewport->zoom <= maxZoom)
{
viewport_invalidate(viewport, sprite->sprite_left, sprite->sprite_top, sprite->sprite_right, sprite->sprite_bottom);
}
}
}
void SpriteBase::Invalidate()
{
int32_t maxZoom = 0;
switch (sprite_identifier)
{
case SpriteIdentifier::Vehicle:
maxZoom = 2;
break;
case SpriteIdentifier::Peep:
maxZoom = 0;
break;
case SpriteIdentifier::Misc:
switch (static_cast<MiscEntityType>(type))
{
case MiscEntityType::CrashedVehicleParticle:
case MiscEntityType::JumpingFountainWater:
case MiscEntityType::JumpingFountainSnow:
maxZoom = 0;
break;
case MiscEntityType::Duck:
maxZoom = 1;
break;
case MiscEntityType::SteamParticle:
case MiscEntityType::MoneyEffect:
case MiscEntityType::ExplosionCloud:
case MiscEntityType::CrashSplash:
case MiscEntityType::ExplosionFlare:
case MiscEntityType::Balloon:
maxZoom = 2;
break;
default:
break;
}
break;
case SpriteIdentifier::Litter:
maxZoom = 0;
break;
default:
break;
}
invalidate_sprite_max_zoom(this, maxZoom);
}
/**
*
* rct2: 0x0069EB13
*/
void reset_sprite_list()
{
gSavedAge = 0;
std::memset(static_cast<void*>(_spriteList), 0, sizeof(_spriteList));
for (int32_t i = 0; i < static_cast<uint8_t>(EntityListId::Count); i++)
{
gSpriteListHead[i] = SPRITE_INDEX_NULL;
gSpriteListCount[i] = 0;
_spriteFlashingList[i] = false;
}
SpriteBase* previous_spr = nullptr;
for (int32_t i = 0; i < MAX_SPRITES; ++i)
{
auto* spr = GetEntity(i);
if (spr == nullptr)
{
continue;
}
spr->sprite_identifier = SpriteIdentifier::Null;
spr->sprite_index = i;
spr->next = SPRITE_INDEX_NULL;
spr->linked_list_index = EntityListId::Free;
if (previous_spr != nullptr)
{
spr->previous = previous_spr->sprite_index;
previous_spr->next = i;
}
else
{
spr->previous = SPRITE_INDEX_NULL;
gSpriteListHead[static_cast<uint8_t>(EntityListId::Free)] = i;
}
_spriteFlashingList[i] = false;
previous_spr = spr;
}
gSpriteListCount[static_cast<uint8_t>(EntityListId::Free)] = MAX_SPRITES;
reset_sprite_spatial_index();
}
/**
*
* 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()
{
std::fill_n(gSpriteSpatialIndex, std::size(gSpriteSpatialIndex), SPRITE_INDEX_NULL);
for (size_t i = 0; i < MAX_SPRITES; i++)
{
auto* spr = GetEntity(i);
if (spr != nullptr && spr->sprite_identifier != SpriteIdentifier::Null)
{
size_t index = GetSpatialIndexOffset(spr->x, spr->y);
uint32_t nextSpriteId = gSpriteSpatialIndex[index];
gSpriteSpatialIndex[index] = spr->sprite_index;
spr->next_in_quadrant = nextSpriteId;
}
}
}
static size_t GetSpatialIndexOffset(int32_t x, int32_t y)
{
size_t index = SPATIAL_INDEX_LOCATION_NULL;
if (x != LOCATION_NULL)
{
x = std::clamp(x, 0, 0xFFFF);
y = std::clamp(y, 0, 0xFFFF);
int16_t flooredX = floor2(x, 32);
uint8_t tileY = y >> 5;
index = (flooredX << 3) | tileY;
}
if (index >= sizeof(gSpriteSpatialIndex))
{
return SPATIAL_INDEX_LOCATION_NULL;
}
return index;
}
#ifndef DISABLE_NETWORK
rct_sprite_checksum sprite_checksum()
{
using namespace Crypt;
// TODO Remove statics, should be one of these per sprite manager / OpenRCT2 context.
// Alternatively, make a new class for this functionality.
static std::unique_ptr<HashAlgorithm<20>> _spriteHashAlg;
rct_sprite_checksum checksum;
try
{
if (_spriteHashAlg == nullptr)
{
_spriteHashAlg = CreateSHA1();
}
_spriteHashAlg->Clear();
for (size_t i = 0; i < MAX_SPRITES; i++)
{
// TODO create a way to copy only the specific type
auto sprite = GetEntity(i);
if (sprite != nullptr && sprite->sprite_identifier != SpriteIdentifier::Null
&& sprite->sprite_identifier != SpriteIdentifier::Misc)
{
// Upconvert it to rct_sprite so that the full size is copied.
auto copy = *reinterpret_cast<rct_sprite*>(sprite);
// Only required for rendering/invalidation, has no meaning to the game state.
copy.generic.sprite_left = copy.generic.sprite_right = copy.generic.sprite_top = copy.generic.sprite_bottom = 0;
copy.generic.sprite_width = copy.generic.sprite_height_negative = copy.generic.sprite_height_positive = 0;
// Next in quadrant might be a misc sprite, set first non-misc sprite in quadrant.
while (auto* nextSprite = GetEntity(copy.generic.next_in_quadrant))
{
if (nextSprite->sprite_identifier == SpriteIdentifier::Misc)
copy.generic.next_in_quadrant = nextSprite->next_in_quadrant;
else
break;
}
if (copy.generic.Is<Peep>())
{
// Name is pointer and will not be the same across clients
copy.peep.Name = {};
// We set this to 0 because as soon the client selects a guest the window will remove the
// invalidation flags causing the sprite checksum to be different than on server, the flag does not
// affect game state.
copy.peep.WindowInvalidateFlags = 0;
}
_spriteHashAlg->Update(&copy, sizeof(copy));
}
}
checksum.raw = _spriteHashAlg->Finish();
}
catch (std::exception& e)
{
log_error("sprite_checksum failed: %s", e.what());
throw;
}
return checksum;
}
#else
rct_sprite_checksum sprite_checksum()
{
return rct_sprite_checksum{};
}
#endif // DISABLE_NETWORK
static void sprite_reset(SpriteBase* sprite)
{
// Need to retain how the sprite is linked in lists
auto llto = sprite->linked_list_index;
uint16_t next = sprite->next;
uint16_t next_in_quadrant = sprite->next_in_quadrant;
uint16_t prev = sprite->previous;
uint16_t sprite_index = sprite->sprite_index;
_spriteFlashingList[sprite_index] = false;
std::memset(sprite, 0, sizeof(rct_sprite));
sprite->linked_list_index = llto;
sprite->next = next;
sprite->next_in_quadrant = next_in_quadrant;
sprite->previous = prev;
sprite->sprite_index = sprite_index;
sprite->sprite_identifier = SpriteIdentifier::Null;
}
/**
* Clears all the unused sprite memory to zero. Probably so that it can be compressed better when saving.
* rct2: 0x0069EBA4
*/
void sprite_clear_all_unused()
{
for (auto sprite : EntityList(EntityListId::Free))
{
sprite_reset(sprite);
sprite->linked_list_index = EntityListId::Free;
// sprite->next_in_quadrant will only end up as zero owing to corruption
// most likely due to previous builds not preserving it when resetting sprites
// We reset it to SPRITE_INDEX_NULL to prevent cycles in the sprite lists
if (sprite->next_in_quadrant == 0)
{
sprite->next_in_quadrant = SPRITE_INDEX_NULL;
}
_spriteFlashingList[sprite->sprite_index] = false;
}
}
static void SpriteSpatialInsert(SpriteBase* sprite, const CoordsXY& newLoc);
static constexpr uint16_t MAX_MISC_SPRITES = 300;
rct_sprite* create_sprite(SpriteIdentifier spriteIdentifier, EntityListId linkedListIndex)
{
if (GetEntityListCount(EntityListId::Free) == 0)
{
// No free sprites.
return nullptr;
}
if (linkedListIndex == EntityListId::Misc)
{
// 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 - GetEntityListCount(EntityListId::Misc);
if (miscSlotsRemaining >= GetEntityListCount(EntityListId::Free))
{
return nullptr;
}
}
auto* sprite = GetEntity(gSpriteListHead[static_cast<uint8_t>(EntityListId::Free)]);
if (sprite == nullptr)
{
return nullptr;
}
move_sprite_to_list(sprite, linkedListIndex);
// Need to reset all sprite data, as the uninitialised values
// may contain garbage and cause a desync later on.
sprite_reset(sprite);
sprite->x = LOCATION_NULL;
sprite->y = LOCATION_NULL;
sprite->z = 0;
sprite->sprite_width = 0x10;
sprite->sprite_height_negative = 0x14;
sprite->sprite_height_positive = 0x8;
sprite->flags = 0;
sprite->sprite_left = LOCATION_NULL;
SpriteSpatialInsert(sprite, { LOCATION_NULL, 0 });
return reinterpret_cast<rct_sprite*>(sprite);
}
rct_sprite* create_sprite(SpriteIdentifier spriteIdentifier)
{
EntityListId linkedListIndex = EntityListId::Free;
switch (spriteIdentifier)
{
case SpriteIdentifier::Vehicle:
linkedListIndex = EntityListId::Vehicle;
break;
case SpriteIdentifier::Peep:
linkedListIndex = EntityListId::Peep;
break;
case SpriteIdentifier::Misc:
linkedListIndex = EntityListId::Misc;
break;
case SpriteIdentifier::Litter:
linkedListIndex = EntityListId::Litter;
break;
default:
Guard::Assert(false, "Invalid sprite identifier: 0x%02X", spriteIdentifier);
return nullptr;
}
return create_sprite(spriteIdentifier, linkedListIndex);
}
/*
* rct2: 0x0069ED0B
* This function moves a sprite to the specified sprite linked list.
* The game uses this list to categorise sprites by type.
*/
static void move_sprite_to_list(SpriteBase* sprite, EntityListId newListIndex)
{
auto oldListIndex = sprite->linked_list_index;
// No need to move if the sprite is already in the desired list
if (oldListIndex == newListIndex)
{
return;
}
// If the sprite is currently the head of the list, the
// sprite following this one becomes the new head of the list.
if (sprite->previous == SPRITE_INDEX_NULL)
{
gSpriteListHead[static_cast<uint8_t>(oldListIndex)] = sprite->next;
}
else
{
// Hook up sprite->previous->next to sprite->next, removing the sprite from its old list
auto previous = GetEntity(sprite->previous);
if (previous == nullptr)
{
log_error("Broken previous entity id. Entity list corrupted!");
}
else
{
previous->next = sprite->next;
}
}
// Similarly, hook up sprite->next->previous to sprite->previous
if (sprite->next != SPRITE_INDEX_NULL)
{
auto next = GetEntity(sprite->next);
if (next == nullptr)
{
log_error("Broken next entity id. Entity list corrupted!");
}
else
{
next->previous = sprite->previous;
}
}
sprite->previous = SPRITE_INDEX_NULL; // We become the new head of the target list, so there's no previous sprite
sprite->linked_list_index = newListIndex;
sprite->next = gSpriteListHead[static_cast<uint8_t>(
newListIndex)]; // This sprite's next sprite is the old head, since we're the new head
gSpriteListHead[static_cast<uint8_t>(newListIndex)] = sprite->sprite_index; // Store this sprite's index as head of its
// new list
if (sprite->next != SPRITE_INDEX_NULL)
{
// Fix the chain by settings sprite->next->previous to sprite_index
auto next = GetEntity(sprite->next);
if (next == nullptr)
{
log_error("Broken next entity id. Entity list corrupted!");
}
else
{
next->previous = sprite->sprite_index;
}
}
// These globals are probably counters for each sprite list?
// Decrement old list counter, increment new list counter.
gSpriteListCount[static_cast<uint8_t>(oldListIndex)]--;
gSpriteListCount[static_cast<uint8_t>(newListIndex)]++;
}
/**
*
* rct2: 0x00673200
*/
void SteamParticle::Update()
{
// Move up 1 z every 3 ticks (Starts after 4 ticks)
Invalidate();
time_to_move++;
if (time_to_move >= 4)
{
time_to_move = 1;
MoveTo({ x, y, z + 1 });
}
frame += 64;
if (frame >= (56 * 64))
{
sprite_remove(this);
}
}
/**
*
* rct2: 0x0067363D
*/
void sprite_misc_explosion_cloud_create(const CoordsXYZ& cloudPos)
{
SpriteGeneric* sprite = &create_sprite(SpriteIdentifier::Misc)->generic;
if (sprite != nullptr)
{
sprite->sprite_width = 44;
sprite->sprite_height_negative = 32;
sprite->sprite_height_positive = 34;
sprite->sprite_identifier = SpriteIdentifier::Misc;
sprite->MoveTo(cloudPos + CoordsXYZ{ 0, 0, 4 });
sprite->type = EnumValue(MiscEntityType::ExplosionCloud);
sprite->frame = 0;
}
}
/**
*
* rct2: 0x00673385
*/
void ExplosionCloud::Update()
{
Invalidate();
frame += 128;
if (frame >= (36 * 128))
{
sprite_remove(this);
}
}
/**
*
* rct2: 0x0067366B
*/
void sprite_misc_explosion_flare_create(const CoordsXYZ& flarePos)
{
SpriteGeneric* sprite = &create_sprite(SpriteIdentifier::Misc)->generic;
if (sprite != nullptr)
{
sprite->sprite_width = 25;
sprite->sprite_height_negative = 85;
sprite->sprite_height_positive = 8;
sprite->sprite_identifier = SpriteIdentifier::Misc;
sprite->MoveTo(flarePos + CoordsXYZ{ 0, 0, 4 });
sprite->type = EnumValue(MiscEntityType::ExplosionFlare);
sprite->frame = 0;
}
}
/**
*
* rct2: 0x006733B4
*/
void ExplosionFlare::Update()
{
Invalidate();
frame += 64;
if (frame >= (124 * 64))
{
sprite_remove(this);
}
}
/**
*
* rct2: 0x006731CD
*/
static void sprite_misc_update(SpriteBase* sprite)
{
switch (static_cast<MiscEntityType>(sprite->type))
{
case MiscEntityType::SteamParticle:
sprite->As<SteamParticle>()->Update();
break;
case MiscEntityType::MoneyEffect:
sprite->As<MoneyEffect>()->Update();
break;
case MiscEntityType::CrashedVehicleParticle:
sprite->As<VehicleCrashParticle>()->Update();
break;
case MiscEntityType::ExplosionCloud:
sprite->As<ExplosionCloud>()->Update();
break;
case MiscEntityType::CrashSplash:
sprite->As<CrashSplashParticle>()->Update();
break;
case MiscEntityType::ExplosionFlare:
sprite->As<ExplosionFlare>()->Update();
break;
case MiscEntityType::JumpingFountainWater:
case MiscEntityType::JumpingFountainSnow:
sprite->As<JumpingFountain>()->Update();
break;
case MiscEntityType::Balloon:
sprite->As<Balloon>()->Update();
break;
case MiscEntityType::Duck:
sprite->As<Duck>()->Update();
break;
default:
break;
}
}
/**
*
* rct2: 0x00672AA4
*/
void sprite_misc_update_all()
{
for (auto entity : EntityList(EntityListId::Misc))
{
sprite_misc_update(entity);
}
}
// Performs a search to ensure that insert keeps next_in_quadrant in sprite_index order
static void SpriteSpatialInsert(SpriteBase* sprite, const CoordsXY& newLoc)
{
size_t newIndex = GetSpatialIndexOffset(newLoc.x, newLoc.y);
auto* next = &gSpriteSpatialIndex[newIndex];
while (sprite->sprite_index < *next && *next != SPRITE_INDEX_NULL)
{
auto sprite2 = GetEntity(*next);
next = &sprite2->next_in_quadrant;
}
sprite->next_in_quadrant = *next;
*next = sprite->sprite_index;
}
static void SpriteSpatialRemove(SpriteBase* sprite)
{
size_t currentIndex = GetSpatialIndexOffset(sprite->x, sprite->y);
auto* index = &gSpriteSpatialIndex[currentIndex];
// This indicates that the spatial index data is incorrect.
if (*index == SPRITE_INDEX_NULL)
{
log_warning("Bad sprite spatial index. Rebuilding the spatial index...");
reset_sprite_spatial_index();
}
auto* sprite2 = GetEntity(*index);
while (sprite != sprite2)
{
index = &sprite2->next_in_quadrant;
if (*index == SPRITE_INDEX_NULL)
{
break;
}
sprite2 = GetEntity(*index);
}
*index = sprite->next_in_quadrant;
}
static void SpriteSpatialMove(SpriteBase* 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);
}
/**
* Moves a sprite to a new location.
* rct2: 0x0069E9D3
*
* @param x (ax)
* @param y (cx)
* @param z (dx)
* @param sprite (esi)
*/
void SpriteBase::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)
{
sprite_left = LOCATION_NULL;
x = loc.x;
y = loc.y;
z = loc.z;
}
else
{
sprite_set_coordinates(loc, this);
Invalidate(); // Invalidate new position.
}
}
void sprite_set_coordinates(const CoordsXYZ& spritePos, SpriteBase* sprite)
{
auto screenCoords = translate_3d_to_2d_with_z(get_current_rotation(), spritePos);
sprite->sprite_left = screenCoords.x - sprite->sprite_width;
sprite->sprite_right = screenCoords.x + sprite->sprite_width;
sprite->sprite_top = screenCoords.y - sprite->sprite_height_negative;
sprite->sprite_bottom = screenCoords.y + sprite->sprite_height_positive;
sprite->x = spritePos.x;
sprite->y = spritePos.y;
sprite->z = spritePos.z;
}
/**
*
* rct2: 0x0069EDB6
*/
void sprite_remove(SpriteBase* sprite)
{
auto peep = sprite->As<Peep>();
if (peep != nullptr)
{
peep->SetName({});
}
move_sprite_to_list(sprite, EntityListId::Free);
sprite->sprite_identifier = SpriteIdentifier::Null;
_spriteFlashingList[sprite->sprite_index] = false;
SpriteSpatialRemove(sprite);
}
static bool litter_can_be_at(const CoordsXYZ& mapPos)
{
TileElement* tileElement;
if (!map_is_location_owned(mapPos))
return false;
tileElement = map_get_first_element_at(mapPos);
if (tileElement == nullptr)
return false;
do
{
if (tileElement->GetType() != TILE_ELEMENT_TYPE_PATH)
continue;
int32_t pathZ = tileElement->GetBaseZ();
if (pathZ < mapPos.z || pathZ >= mapPos.z + 32)
continue;
return !tile_element_is_underground(tileElement);
} while (!(tileElement++)->IsLastForTile());
return false;
}
/**
*
* rct2: 0x0067375D
*/
void litter_create(const CoordsXYZD& litterPos, int32_t type)
{
if (gCheatsDisableLittering)
return;
auto offsetLitterPos = litterPos
+ CoordsXY{ CoordsDirectionDelta[litterPos.direction >> 3].x / 8,
CoordsDirectionDelta[litterPos.direction >> 3].y / 8 };
if (!litter_can_be_at(offsetLitterPos))
return;
if (GetEntityListCount(EntityListId::Litter) >= 500)
{
Litter* newestLitter = nullptr;
uint32_t newestLitterCreationTick = 0;
for (auto litter : EntityList<Litter>(EntityListId::Litter))
{
if (newestLitterCreationTick <= litter->creationTick)
{
newestLitterCreationTick = litter->creationTick;
newestLitter = litter;
}
}
if (newestLitter != nullptr)
{
newestLitter->Invalidate();
sprite_remove(newestLitter);
}
}
Litter* litter = reinterpret_cast<Litter*>(create_sprite(SpriteIdentifier::Litter));
if (litter == nullptr)
return;
litter->sprite_direction = offsetLitterPos.direction;
litter->sprite_width = 6;
litter->sprite_height_negative = 6;
litter->sprite_height_positive = 3;
litter->sprite_identifier = SpriteIdentifier::Litter;
litter->type = type;
litter->MoveTo(offsetLitterPos);
litter->creationTick = gScenarioTicks;
}
/**
*
* rct2: 0x006738E1
*/
void litter_remove_at(const CoordsXYZ& litterPos)
{
for (auto litter : EntityTileList<Litter>(litterPos))
{
if (abs(litter->z - litterPos.z) <= 16)
{
if (abs(litter->x - litterPos.x) <= 8 && abs(litter->y - litterPos.y) <= 8)
{
litter->Invalidate();
sprite_remove(litter);
}
}
}
}
/**
* 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 (uint16_t i = 0; i < MAX_SPRITES; i++)
{
auto* entity = GetEntity(i);
if (entity->Is<Balloon>())
{
sprite_remove(entity);
removed++;
}
else if (entity->Is<Duck>())
{
auto* duck = entity->As<Duck>();
if (duck->IsFlying())
{
duck->Remove();
removed++;
}
}
else if (entity->Is<MoneyEffect>())
{
sprite_remove(entity);
removed++;
}
}
return removed;
}
/**
* Determines whether it's worth tweening a sprite or not when frame smoothing is on.
*/
static bool sprite_should_tween(SpriteBase* sprite)
{
switch (sprite->sprite_identifier)
{
case SpriteIdentifier::Peep:
case SpriteIdentifier::Vehicle:
return true;
case SpriteIdentifier::Misc:
case SpriteIdentifier::Litter:
case SpriteIdentifier::Null:
return false;
}
return false;
}
static void store_sprite_locations(CoordsXYZ* sprite_locations)
{
for (uint16_t i = 0; i < MAX_SPRITES; i++)
{
// skip going through `get_sprite` to not get stalled on assert,
// this can get very expensive for busy parks with uncap FPS option on
const rct_sprite* sprite = &_spriteList[i];
sprite_locations[i].x = sprite->generic.x;
sprite_locations[i].y = sprite->generic.y;
sprite_locations[i].z = sprite->generic.z;
}
}
void sprite_position_tween_store_a()
{
store_sprite_locations(_spritelocations1);
}
void sprite_position_tween_store_b()
{
store_sprite_locations(_spritelocations2);
}
void sprite_position_tween_all(float alpha)
{
const float inv = (1.0f - alpha);
for (uint16_t i = 0; i < MAX_SPRITES; i++)
{
auto* sprite = GetEntity(i);
if (sprite != nullptr && sprite_should_tween(sprite))
{
auto posA = _spritelocations1[i];
auto posB = _spritelocations2[i];
if (posA == posB)
{
continue;
}
sprite_set_coordinates(
{ static_cast<int32_t>(std::round(posB.x * alpha + posA.x * inv)),
static_cast<int32_t>(std::round(posB.y * alpha + posA.y * inv)),
static_cast<int32_t>(std::round(posB.z * alpha + posA.z * inv)) },
sprite);
sprite->Invalidate();
}
}
}
/**
* Restore the real positions of the sprites so they aren't left at the mid-tween positions
*/
void sprite_position_tween_restore()
{
for (uint16_t i = 0; i < MAX_SPRITES; i++)
{
auto* sprite = GetEntity(i);
if (sprite != nullptr && sprite_should_tween(sprite))
{
sprite->Invalidate();
auto pos = _spritelocations2[i];
sprite_set_coordinates(pos, sprite);
}
}
}
void sprite_position_tween_reset()
{
for (uint16_t i = 0; i < MAX_SPRITES; i++)
{
auto* sprite = GetEntity(i);
if (sprite == nullptr)
{
continue;
}
_spritelocations1[i].x = _spritelocations2[i].x = sprite->x;
_spritelocations1[i].y = _spritelocations2[i].y = sprite->y;
_spritelocations1[i].z = _spritelocations2[i].z = sprite->z;
}
}
void sprite_set_flashing(SpriteBase* sprite, bool flashing)
{
assert(sprite->sprite_index < MAX_SPRITES);
_spriteFlashingList[sprite->sprite_index] = flashing;
}
bool sprite_get_flashing(SpriteBase* sprite)
{
assert(sprite->sprite_index < MAX_SPRITES);
return _spriteFlashingList[sprite->sprite_index];
}
static SpriteBase* find_sprite_list_cycle(uint16_t sprite_idx)
{
if (sprite_idx == SPRITE_INDEX_NULL)
{
return nullptr;
}
const SpriteBase* fast = GetEntity(sprite_idx);
const SpriteBase* slow = fast;
bool increment_slow = false;
SpriteBase* cycle_start = nullptr;
while (fast->sprite_index != SPRITE_INDEX_NULL)
{
// increment fast every time, unless reached the end
if (fast->next == SPRITE_INDEX_NULL)
{
break;
}
else
{
fast = GetEntity(fast->next);
}
// increment slow only every second iteration
if (increment_slow)
{
slow = GetEntity(slow->next);
}
increment_slow = !increment_slow;
if (fast == slow)
{
cycle_start = GetEntity(slow->sprite_index);
break;
}
}
return cycle_start;
}
static bool index_is_in_list(uint16_t index, EntityListId sl)
{
for (auto entity : EntityList(sl))
{
if (entity->sprite_index == index)
{
return true;
}
}
return false;
}
int32_t check_for_sprite_list_cycles(bool fix)
{
for (int32_t i = 0; i < static_cast<uint8_t>(EntityListId::Count); i++)
{
auto* cycle_start = find_sprite_list_cycle(gSpriteListHead[i]);
if (cycle_start != nullptr)
{
if (fix)
{
// Fix head list, but only in reverse order
// This is likely not needed, but just in case
auto head = GetEntity(gSpriteListHead[i]);
if (head == nullptr)
{
log_error("SpriteListHead is corrupted!");
return -1;
}
head->previous = SPRITE_INDEX_NULL;
// Store the leftover part of cycle to be fixed
uint16_t cycle_next = cycle_start->next;
// Break the cycle
cycle_start->next = SPRITE_INDEX_NULL;
// Now re-add remainder of the cycle back to list, safely.
// Add each sprite to the list until we encounter one that is already part of the list.
while (!index_is_in_list(cycle_next, static_cast<EntityListId>(i)))
{
auto* spr = GetEntity(cycle_next);
if (spr == nullptr)
{
log_error("EntityList is corrupted!");
return -1;
}
cycle_start->next = cycle_next;
spr->previous = cycle_start->sprite_index;
cycle_next = spr->next;
spr->next = SPRITE_INDEX_NULL;
cycle_start = spr;
}
}
return i;
}
}
return -1;
}
/**
* Finds and fixes null sprites that are not reachable via EntityListId::Free list.
*
* @return count of disjoint sprites found
*/
int32_t fix_disjoint_sprites()
{
// Find reachable sprites
bool reachable[MAX_SPRITES] = { false };
SpriteBase* null_list_tail = nullptr;
for (uint16_t sprite_idx = gSpriteListHead[static_cast<uint8_t>(EntityListId::Free)]; sprite_idx != SPRITE_INDEX_NULL;)
{
reachable[sprite_idx] = true;
// cache the tail, so we don't have to walk the list twice
null_list_tail = GetEntity(sprite_idx);
if (null_list_tail == nullptr)
{
log_error("Broken Entity list");
sprite_idx = SPRITE_INDEX_NULL;
return 0;
}
sprite_idx = null_list_tail->next;
}
int32_t count = 0;
// Find all null sprites
for (uint16_t sprite_idx = 0; sprite_idx < MAX_SPRITES; sprite_idx++)
{
auto* spr = GetEntity(sprite_idx);
if (spr != nullptr && spr->sprite_identifier == SpriteIdentifier::Null)
{
openrct2_assert(null_list_tail != nullptr, "Null list is empty, yet found null sprites");
spr->sprite_index = sprite_idx;
if (!reachable[sprite_idx])
{
// Add the sprite directly to the list
if (null_list_tail == nullptr)
{
gSpriteListHead[static_cast<uint8_t>(EntityListId::Free)] = sprite_idx;
spr->previous = SPRITE_INDEX_NULL;
}
else
{
null_list_tail->next = sprite_idx;
spr->previous = null_list_tail->sprite_index;
}
spr->next = SPRITE_INDEX_NULL;
null_list_tail = spr;
count++;
reachable[sprite_idx] = true;
}
}
}
return count;
}