diff --git a/dGame/dComponents/CMakeLists.txt b/dGame/dComponents/CMakeLists.txt index e82ec360e..b92fea97e 100644 --- a/dGame/dComponents/CMakeLists.txt +++ b/dGame/dComponents/CMakeLists.txt @@ -42,6 +42,7 @@ set(DGAME_DCOMPONENTS_SOURCES "SimplePhysicsComponent.cpp" "SkillComponent.cpp" "SoundTriggerComponent.cpp" + "SpawnerComponent.cpp" "SwitchComponent.cpp" "TriggerComponent.cpp" "HavokVehiclePhysicsComponent.cpp" diff --git a/dGame/dComponents/SpawnerComponent.cpp b/dGame/dComponents/SpawnerComponent.cpp new file mode 100644 index 000000000..afd1d60c8 --- /dev/null +++ b/dGame/dComponents/SpawnerComponent.cpp @@ -0,0 +1,17 @@ +#include "SpawnerComponent.h" + +#include "Entity.h" +#include "Game.h" +#include "Spawner.h" +#include "dZoneManager.h" + +SpawnerComponent::SpawnerComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {} + +// The zone manager stops indexing the spawner before it goes with the object +SpawnerComponent::~SpawnerComponent() { + if (m_Spawner && Game::zoneManager) Game::zoneManager->ForgetSpawner(m_Parent->GetObjectID(), m_Spawner.get()); +} + +void SpawnerComponent::SetSpawner(std::unique_ptr spawner) { + m_Spawner = std::move(spawner); +} diff --git a/dGame/dComponents/SpawnerComponent.h b/dGame/dComponents/SpawnerComponent.h index 34fabe881..7558ae30c 100644 --- a/dGame/dComponents/SpawnerComponent.h +++ b/dGame/dComponents/SpawnerComponent.h @@ -1,17 +1,29 @@ #ifndef SPAWNERCOMPONENT_H #define SPAWNERCOMPONENT_H +#include + #include "Component.h" #include "eReplicaComponentType.h" +class Spawner; + /** - * The spawner component (10) of a spawner object (LOT 176), made from the registry as the client makes it. + * The spawner component (10) of a spawner object (LOT 176), made from the registry as the client makes it. It owns the + * spawner (its nodes, what it spawned and its timers); the zone manager indexes spawners by the object's ID. */ class SpawnerComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::SPAWNER; - SpawnerComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {} + SpawnerComponent(Entity* parent, const int32_t componentID); + ~SpawnerComponent() override; + + void SetSpawner(std::unique_ptr spawner); + Spawner* GetSpawner() const { return m_Spawner.get(); } + +private: + std::unique_ptr m_Spawner; }; #endif // SPAWNERCOMPONENT_H diff --git a/dZoneManager/dZoneManager.cpp b/dZoneManager/dZoneManager.cpp index ea19dc886..a4b22c802 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -1,6 +1,7 @@ #include "Game.h" #include "dCommonVars.h" #include "dZoneManager.h" +#include "SpawnerComponent.h" #include "EntityManager.h" #include "Logger.h" #include "dConfig.h" @@ -74,12 +75,8 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { dZoneManager::~dZoneManager() { if (m_pZone) delete m_pZone; - for (auto* spawner : m_Spawners | std::views::values) { - if (spawner) { - delete spawner; - spawner = nullptr; - } - } + // Each spawner belongs to its object's SpawnerComponent + m_Spawners.clear(); } Zone* dZoneManager::GetZoneMut() const { @@ -118,7 +115,8 @@ LWOOBJID dZoneManager::MakeSpawner(SpawnerInfo info) { info.spawnerID = objectId; } - auto* spawner = new Spawner(info); + auto spawner = std::make_unique(info); + auto* const spawnerPtr = spawner.get(); EntityInfo entityInfo{}; @@ -127,9 +125,14 @@ LWOOBJID dZoneManager::MakeSpawner(SpawnerInfo info) { auto* entity = Game::entityManager->CreateEntity(entityInfo, nullptr, nullptr, false, objectId); + // The spawner object's SpawnerComponent (from the registry) owns the spawner + auto* spawnerComponent = entity->GetComponent(); + if (!spawnerComponent) spawnerComponent = entity->AddComponent(-1); + spawnerComponent->SetSpawner(std::move(spawner)); + Game::entityManager->ConstructEntity(entity); - AddSpawner(objectId, spawner); + AddSpawner(objectId, spawnerPtr); return objectId; } @@ -163,9 +166,13 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) { LOG("Destroyed spawner (%llu)", id); + // The spawner goes with its object's SpawnerComponent m_Spawners.erase(id); +} - delete spawner; +void dZoneManager::ForgetSpawner(const LWOOBJID id, const Spawner* spawner) { + const auto it = m_Spawners.find(id); + if (it != m_Spawners.end() && it->second == spawner) m_Spawners.erase(it); } diff --git a/dZoneManager/dZoneManager.h b/dZoneManager/dZoneManager.h index 2d1fd5cb7..ba2b210ac 100644 --- a/dZoneManager/dZoneManager.h +++ b/dZoneManager/dZoneManager.h @@ -40,6 +40,8 @@ public: LWOOBJID MakeSpawner(SpawnerInfo info); Spawner* GetSpawner(LWOOBJID id); void RemoveSpawner(LWOOBJID id); + // Called by a SpawnerComponent going away: stops indexing its spawner + void ForgetSpawner(LWOOBJID id, const Spawner* spawner); std::vector GetSpawnersByName(const std::string& spawnerName); std::vector GetSpawnersInGroup(const std::string& group); // Every spawner in the zone (live events pick walkable places from their nodes) diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 4f8185d68..6763870dd 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -22,6 +22,7 @@ set(DCOMPONENTS_TESTS "CapturePropertyTests.cpp" "ItemRemovalTests.cpp" "LootMetricsTests.cpp" + "SpawnerComponentTests.cpp" ) # Get the folder name and prepend it to the files above diff --git a/tests/dGameTests/dComponentsTests/SpawnerComponentTests.cpp b/tests/dGameTests/dComponentsTests/SpawnerComponentTests.cpp new file mode 100644 index 000000000..2c2e9a430 --- /dev/null +++ b/tests/dGameTests/dComponentsTests/SpawnerComponentTests.cpp @@ -0,0 +1,55 @@ +#include "GameDependencies.h" + +#include "CDClientManager.h" +#include "CDComponentsRegistryTable.h" +#include "Entity.h" +#include "Spawner.h" +#include "SpawnerComponent.h" +#include "dZoneManager.h" + +#include + +// A spawner object's SpawnerComponent owns its spawner; the zone manager only indexes it. +class SpawnerComponentTests : public GameDependenciesTest { +protected: + void SetUp() override { + SetUpDependencies(); + // The entity's own LOT has no components, so it doesn't look anything up in a database + CDClientManager::GetEntriesMutable().insert_or_assign(static_cast(info.lot), 0); + } + + void TearDown() override { TearDownDependencies(); } + + static SpawnerInfo InactiveSpawner(const LWOOBJID id) { + SpawnerInfo spawnerInfo{}; + spawnerInfo.spawnerID = id; + spawnerInfo.activeOnLoad = false; + return spawnerInfo; + } +}; + +TEST_F(SpawnerComponentTests, TheZoneManagerFindsTheComponentsSpawner) { + constexpr LWOOBJID id = 1234; + auto entity = std::make_unique(id, info); + auto* const spawnerComponent = entity->AddComponent(-1); + auto spawner = std::make_unique(InactiveSpawner(id)); + auto* const spawnerPtr = spawner.get(); + spawnerComponent->SetSpawner(std::move(spawner)); + Game::zoneManager->AddSpawner(id, spawnerPtr); + + EXPECT_EQ(spawnerComponent->GetSpawner(), spawnerPtr); + EXPECT_EQ(Game::zoneManager->GetSpawner(id), spawnerPtr); +} + +TEST_F(SpawnerComponentTests, TheSpawnerGoesWithItsObject) { + constexpr LWOOBJID id = 1235; + auto entity = std::make_unique(id, info); + auto* const spawnerComponent = entity->AddComponent(-1); + auto spawner = std::make_unique(InactiveSpawner(id)); + Game::zoneManager->AddSpawner(id, spawner.get()); + spawnerComponent->SetSpawner(std::move(spawner)); + + entity.reset(); + + EXPECT_EQ(Game::zoneManager->GetSpawner(id), nullptr); +}