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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-01 10:31:18 -05:00
parent 9da7b11456
commit 2d7b5cca13
7 changed files with 106 additions and 11 deletions

View File

@@ -42,6 +42,7 @@ set(DGAME_DCOMPONENTS_SOURCES
"SimplePhysicsComponent.cpp"
"SkillComponent.cpp"
"SoundTriggerComponent.cpp"
"SpawnerComponent.cpp"
"SwitchComponent.cpp"
"TriggerComponent.cpp"
"HavokVehiclePhysicsComponent.cpp"

View File

@@ -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> spawner) {
m_Spawner = std::move(spawner);
}

View File

@@ -1,17 +1,29 @@
#ifndef SPAWNERCOMPONENT_H
#define SPAWNERCOMPONENT_H
#include <memory>
#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);
Spawner* GetSpawner() const { return m_Spawner.get(); }
private:
std::unique_ptr<Spawner> m_Spawner;
};
#endif // SPAWNERCOMPONENT_H

View File

@@ -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<Spawner>(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<SpawnerComponent>();
if (!spawnerComponent) spawnerComponent = entity->AddComponent<SpawnerComponent>(-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);
}

View File

@@ -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<Spawner*> GetSpawnersByName(const std::string& spawnerName);
std::vector<Spawner*> GetSpawnersInGroup(const std::string& group);
// Every spawner in the zone (live events pick walkable places from their nodes)

View File

@@ -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

View File

@@ -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 <gtest/gtest.h>
// 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<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(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<Entity>(id, info);
auto* const spawnerComponent = entity->AddComponent<SpawnerComponent>(-1);
auto spawner = std::make_unique<Spawner>(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<Entity>(id, info);
auto* const spawnerComponent = entity->AddComponent<SpawnerComponent>(-1);
auto spawner = std::make_unique<Spawner>(InactiveSpawner(id));
Game::zoneManager->AddSpawner(id, spawner.get());
spawnerComponent->SetSpawner(std::move(spawner));
entity.reset();
EXPECT_EQ(Game::zoneManager->GetSpawner(id), nullptr);
}