/***************************************************************************** * Copyright (c) 2014-2025 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 "EntityRegistry.h" #include "../Diagnostic.h" #include "../Game.h" #include "../GameState.h" #include "../core/Algorithm.hpp" #include "../core/ChecksumStream.h" #include "../core/Crypt.h" #include "../core/DataSerialiser.h" #include "../core/Guard.hpp" #include "../core/MemoryStream.h" #include "../core/String.hpp" #include "../entity/EntityList.h" #include "../entity/Peep.h" #include "../entity/Staff.h" #include "../interface/Viewport.h" #include "../peep/RideUseSystem.h" #include "../profiling/Profiling.h" #include "../ride/Vehicle.h" #include "Balloon.h" #include "Duck.h" #include "EntityTweener.h" #include "Fountain.h" #include "MoneyEffect.h" #include "Particle.h" #include #include #include #include #include using namespace OpenRCT2; static std::array, EnumValue(EntityType::Count)> gEntityLists; static std::vector _freeIdList; static bool _entityFlashingList[kMaxEntities]; static constexpr const uint32_t kSpatialIndexSize = (kMaximumMapSizeTechnical * kMaximumMapSizeTechnical) + 1; static constexpr uint32_t kSpatialIndexNullBucket = kSpatialIndexSize - 1; static constexpr uint32_t kInvalidSpatialIndex = 0xFFFFFFFFu; static constexpr uint32_t kSpatialIndexDirtyMask = 1u << 31; static std::array, kSpatialIndexSize> gEntitySpatialIndex; static void FreeEntity(EntityBase& entity); static constexpr uint32_t ComputeSpatialIndex(const CoordsXY& loc) { if (loc.IsNull()) return kSpatialIndexNullBucket; // NOTE: The input coordinate is rotated and can have negative components. const auto tileX = std::abs(loc.x) / kCoordsXYStep; const auto tileY = std::abs(loc.y) / kCoordsXYStep; if (tileX >= kMaximumMapSizeTechnical || tileY >= kMaximumMapSizeTechnical) return kSpatialIndexNullBucket; return tileX * kMaximumMapSizeTechnical + tileY; } static constexpr uint32_t GetSpatialIndex(EntityBase* entity) { return entity->SpatialIndex & ~kSpatialIndexDirtyMask; } 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; } } uint16_t GetEntityListCount(EntityType type) { return static_cast(gEntityLists[EnumValue(type)].size()); } uint16_t GetNumFreeEntities() { return static_cast(_freeIdList.size()); } std::string EntitiesChecksum::ToString() const { return String::StringFromHex(raw); } EntityBase* TryGetEntity(EntityId entityIndex) { auto& gameState = getGameState(); const auto idx = entityIndex.ToUnderlying(); return idx >= kMaxEntities ? nullptr : &gameState.entities[idx].base; } EntityBase* GetEntity(EntityId entityIndex) { if (entityIndex.IsNull()) { return nullptr; } Guard::Assert(entityIndex.ToUnderlying() < kMaxEntities, "Tried getting entity %u", entityIndex.ToUnderlying()); return TryGetEntity(entityIndex); } const std::vector& GetEntityTileList(const CoordsXY& spritePos) { return gEntitySpatialIndex[ComputeSpatialIndex(spritePos)]; } static void ResetEntityLists() { for (auto& list : gEntityLists) { list.clear(); } } static void ResetFreeIds() { _freeIdList.clear(); _freeIdList.resize(kMaxEntities); // List needs to be back to front to simplify removing auto nextId = 0; std::for_each(std::rbegin(_freeIdList), std::rend(_freeIdList), [&](auto& elem) { elem = EntityId::FromUnderlying(nextId); nextId++; }); } const std::list& GetEntityList(const EntityType id) { return gEntityLists[EnumValue(id)]; } /** * * rct2: 0x0069EB13 */ void ResetAllEntities() { // Free all associated Entity pointers prior to zeroing memory for (int32_t i = 0; i < kMaxEntities; ++i) { auto* spr = GetEntity(EntityId::FromUnderlying(i)); if (spr == nullptr) { continue; } FreeEntity(*spr); } auto& gameState = getGameState(); std::fill(std::begin(gameState.entities), std::end(gameState.entities), Entity_t()); OpenRCT2::RideUse::GetHistory().Clear(); OpenRCT2::RideUse::GetTypeHistory().Clear(); for (int32_t i = 0; i < kMaxEntities; ++i) { auto* spr = GetEntity(EntityId::FromUnderlying(i)); if (spr == nullptr) { continue; } spr->Type = EntityType::Null; spr->Id = EntityId::FromUnderlying(i); _entityFlashingList[i] = false; } ResetEntityLists(); ResetFreeIds(); ResetEntitySpatialIndices(); } static void EntitySpatialInsert(EntityBase* entity, 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 ResetEntitySpatialIndices() { for (auto& vec : gEntitySpatialIndex) { vec.clear(); } for (EntityId::UnderlyingType i = 0; i < kMaxEntities; i++) { auto* entity = GetEntity(EntityId::FromUnderlying(i)); if (entity != nullptr && entity->Type != EntityType::Null) { EntitySpatialInsert(entity, { entity->x, entity->y }); } } } #ifndef DISABLE_NETWORK template void NetworkSerialseEntityType(DataSerialiser& ds) { for (auto* ent : EntityList()) { ent->Serialise(ds); } } template void NetworkSerialiseEntityTypes(DataSerialiser& ds) { (NetworkSerialseEntityType(ds), ...); } EntitiesChecksum GetAllEntitiesChecksum() { EntitiesChecksum checksum{}; OpenRCT2::ChecksumStream ms(checksum.raw); DataSerialiser ds(true, ms); NetworkSerialiseEntityTypes(ds); return checksum; } #else EntitiesChecksum GetAllEntitiesChecksum() { return EntitiesChecksum{}; } #endif // DISABLE_NETWORK static void EntityReset(EntityBase* entity) { // Need to retain how the sprite is linked in lists auto entityIndex = entity->Id; _entityFlashingList[entityIndex.ToUnderlying()] = false; Entity_t* tempEntity = reinterpret_cast(entity); *tempEntity = Entity_t(); entity->Id = entityIndex; entity->Type = EntityType::Null; } static constexpr uint16_t kMaxMiscEntities = 1600; 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->Id), entity->Id); } static void AddToFreeList(EntityId 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 = BinaryFind(std::begin(list), std::end(list), entity->Id); if (ptr != std::end(list)) { 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. EntityReset(base); base->Type = type; AddToEntityList(base); base->x = kLocationNull; base->y = kLocationNull; base->z = 0; base->SpriteData.Width = 0x10; base->SpriteData.HeightMin = 0x14; base->SpriteData.HeightMax = 0x8; base->SpriteData.SpriteRect = {}; base->SpatialIndex = kInvalidSpatialIndex; EntitySpatialInsert(base, { kLocationNull, 0 }); } EntityBase* CreateEntity(EntityType type) { if (_freeIdList.size() == 0) { // No free sprites. return nullptr; } if (EntityTypeIsMiscEntity(type)) { // Misc sprites are commonly used for effects, give other entity types higher priority. if (GetMiscEntityCount() >= kMaxMiscEntities) { return nullptr; } // If there are less than kMaxMiscEntities free slots, ensure other entities can be created. if (_freeIdList.size() < kMaxMiscEntities) { return nullptr; } } auto* entity = GetEntity(_freeIdList.back()); if (entity == nullptr) { return nullptr; } _freeIdList.pop_back(); PrepareNewEntity(entity, type); return entity; } EntityBase* CreateEntityAt(const EntityId index, const EntityType type) { auto id = BinaryFind(std::rbegin(_freeIdList), std::rend(_freeIdList), index); if (id == std::rend(_freeIdList)) { 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 UpdateAllMiscEntities() { PROFILED_FUNCTION(); MiscUpdateAllTypes< SteamParticle, MoneyEffect, VehicleCrashParticle, ExplosionCloud, CrashSplashParticle, ExplosionFlare, JumpingFountain, Balloon, Duck>(); } void UpdateMoneyEffect() { MiscUpdateAllTypes(); } // Performs a search to ensure that insert keeps next_in_quadrant in sprite_index order static void EntitySpatialInsert(EntityBase* entity, const CoordsXY& newLoc) { const auto newIndex = ComputeSpatialIndex(newLoc); auto& spatialVector = gEntitySpatialIndex[newIndex]; auto index = std::lower_bound(std::begin(spatialVector), std::end(spatialVector), entity->Id); spatialVector.insert(index, entity->Id); entity->SpatialIndex = newIndex; } static void EntitySpatialRemove(EntityBase* entity) { const auto currentIndex = GetSpatialIndex(entity); auto& spatialVector = gEntitySpatialIndex[currentIndex]; auto index = BinaryFind(std::begin(spatialVector), std::end(spatialVector), entity->Id); if (index != std::end(spatialVector)) { spatialVector.erase(index, index + 1); } else { LOG_WARNING("Bad sprite spatial index. Rebuilding the spatial index..."); ResetEntitySpatialIndices(); } entity->SpatialIndex = kInvalidSpatialIndex; } void UpdateEntitiesSpatialIndex() { for (auto& entityList : gEntityLists) { for (auto& entityId : entityList) { auto* entity = TryGetEntity(entityId); if (entity == nullptr || entity->Type == EntityType::Null) continue; if (entity->SpatialIndex & kSpatialIndexDirtyMask) { if (entity->SpatialIndex != kInvalidSpatialIndex) { EntitySpatialRemove(entity); } EntitySpatialInsert(entity, { entity->x, entity->y }); } } } } CoordsXYZ EntityBase::GetLocation() const { return { x, y, z }; } void EntityBase::SetLocation(const CoordsXYZ& newLocation) { if (GetLocation() == newLocation) { // No change, this can happen quite often when the entity is interpolated. return; } x = newLocation.x; y = newLocation.y; z = newLocation.z; if (SpatialIndex & kSpatialIndexDirtyMask) { // Already marked as dirty. return; } const auto newSpatialIndex = ComputeSpatialIndex({ x, y }); if (newSpatialIndex == GetSpatialIndex(this)) { // Avoid marking it dirty when we don't leave the current tile. return; } SpatialIndex |= kSpatialIndexDirtyMask; } static void EntitySetCoordinates(const CoordsXYZ& entityPos, EntityBase* entity) { auto screenCoords = Translate3DTo2DWithZ(GetCurrentRotation(), entityPos); entity->SpriteData.SpriteRect = ScreenRect( screenCoords - ScreenCoordsXY{ entity->SpriteData.Width, entity->SpriteData.HeightMin }, screenCoords + ScreenCoordsXY{ entity->SpriteData.Width, entity->SpriteData.HeightMax }); entity->SetLocation(entityPos); } void EntityBase::MoveTo(const CoordsXYZ& newLocation) { if (x != kLocationNull) { // Invalidate old position. Invalidate(); } auto loc = newLocation; if (!MapIsLocationValid(loc)) { loc.x = kLocationNull; } if (loc.x == kLocationNull) { SetLocation(loc); } else { EntitySetCoordinates(loc, this); Invalidate(); // Invalidate new position. } } /** * 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({}); guest->GuestNextInQueue = EntityId::GetNull(); OpenRCT2::RideUse::GetHistory().RemoveHandle(guest->Id); OpenRCT2::RideUse::GetTypeHistory().RemoveHandle(guest->Id); } } /** * * rct2: 0x0069EDB6 */ void EntityRemove(EntityBase* entity) { FreeEntity(*entity); EntityTweener::Get().RemoveEntity(entity); RemoveFromEntityList(entity); // remove from existing list AddToFreeList(entity->Id); EntitySpatialRemove(entity); EntityReset(entity); } /** * Loops through all floating entities and removes them. * Returns the amount of removed objects as feedback. */ uint16_t RemoveFloatingEntities() { uint16_t removed = 0; for (auto* balloon : EntityList()) { EntityRemove(balloon); removed++; } for (auto* duck : EntityList()) { if (duck->IsFlying()) { EntityRemove(duck); removed++; } } for (auto* money : EntityList()) { EntityRemove(money); removed++; } return removed; } void EntitySetFlashing(EntityBase* entity, bool flashing) { assert(entity->Id.ToUnderlying() < kMaxEntities); _entityFlashingList[entity->Id.ToUnderlying()] = flashing; } bool EntityGetFlashing(EntityBase* entity) { assert(entity->Id.ToUnderlying() < kMaxEntities); return _entityFlashingList[entity->Id.ToUnderlying()]; }