From 2d7b5cca13fc993a112316789e14e98d8cfe35f1 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Thu, 1 Oct 2026 10:31:18 -0500 Subject: [PATCH] refactor(spawner): the spawner object's SpawnerComponent owns its spawner The zone manager keeps an index of spawners by object ID; the spawner now belongs to the SpawnerComponent of its LOT 176 object and goes with it (the component tells the zone manager to stop indexing it). Spawning, timers and the update order are unchanged. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/CMakeLists.txt | 1 + dGame/dComponents/SpawnerComponent.cpp | 17 ++++++ dGame/dComponents/SpawnerComponent.h | 16 +++++- dZoneManager/dZoneManager.cpp | 25 ++++++--- dZoneManager/dZoneManager.h | 2 + .../dComponentsTests/CMakeLists.txt | 1 + .../SpawnerComponentTests.cpp | 55 +++++++++++++++++++ 7 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 dGame/dComponents/SpawnerComponent.cpp create mode 100644 tests/dGameTests/dComponentsTests/SpawnerComponentTests.cpp 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); +}