feat: spawner weights

This commit is contained in:
David Markowitz
2026-06-19 18:42:34 -07:00
parent 308412f46e
commit a720895bee
5 changed files with 93 additions and 53 deletions

View File

@@ -289,6 +289,14 @@ struct LwoNameValue {
this->Erase(GeneralUtils::ASCIIToUTF16(key)); this->Erase(GeneralUtils::ASCIIToUTF16(key));
} }
ValueType::iterator find(const ValueType::key_type& key) {
return this->values.find(key);
}
ValueType::const_iterator find(const ValueType::key_type& key) const {
return this->values.find(key);
}
LwoNameValue() = default; LwoNameValue() = default;
LwoNameValue(const LwoNameValue& other) { LwoNameValue(const LwoNameValue& other) {

View File

@@ -14,7 +14,7 @@ void VisToggleNotifierServer::OnMissionDialogueOK(Entity* self, Entity* target,
auto spawners = Game::zoneManager->GetSpawnersByName(itr->second); auto spawners = Game::zoneManager->GetSpawnersByName(itr->second);
if (spawners.empty()) return; if (spawners.empty()) return;
for (const auto spawner : spawners) { for (const auto spawner : spawners) {
auto spawnedObjIds = spawner->GetSpawnedObjectIDs(); const auto& spawnedObjIds = spawner->GetSpawnedObjectIDs();
for (const auto& objId : spawnedObjIds) { for (const auto& objId : spawnedObjIds) {
GameMessages::SendNotifyClientObject(objId, u"SetVisibility", visible); GameMessages::SendNotifyClientObject(objId, u"SetVisibility", visible);
} }

View File

@@ -6,8 +6,10 @@
#include <functional> #include <functional>
#include "GeneralUtils.h" #include "GeneralUtils.h"
#include "dZoneManager.h" #include "dZoneManager.h"
#include <algorithm>
#include <ranges>
Spawner::Spawner(const SpawnerInfo info) { Spawner::Spawner(const SpawnerInfo& info) {
m_Info = info; m_Info = info;
m_Active = m_Info.activeOnLoad && info.spawnActivator; m_Active = m_Info.activeOnLoad && info.spawnActivator;
m_EntityInfo = EntityInfo(); m_EntityInfo = EntityInfo();
@@ -62,10 +64,6 @@ Spawner::Spawner(const SpawnerInfo info) {
} }
} }
Spawner::~Spawner() {
}
Entity* Spawner::Spawn() { Entity* Spawner::Spawn() {
std::vector<SpawnerNode*> freeNodes; std::vector<SpawnerNode*> freeNodes;
for (SpawnerNode* node : m_Info.nodes) { for (SpawnerNode* node : m_Info.nodes) {
@@ -77,9 +75,25 @@ Entity* Spawner::Spawn() {
return Spawn(freeNodes); return Spawn(freeNodes);
} }
Entity* Spawner::Spawn(std::vector<SpawnerNode*> freeNodes, const bool force) { Entity* Spawner::Spawn(const std::vector<SpawnerNode*>& freeNodes, const bool force) {
if (force || ((m_Entities.size() < m_Info.amountMaintained) && (freeNodes.size() > 0) && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) { Entity* spawnedEntity = nullptr;
SpawnerNode* spawnNode = freeNodes[GeneralUtils::GenerateRandomNumber<int>(0, freeNodes.size() - 1)]; if (force || ((m_Entities.size() < m_Info.amountMaintained) && !freeNodes.empty() && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) {
// first sum the weights we were provided
int32_t spawnWeight = 0;
for (const auto* const node : freeNodes) spawnWeight += node->weight;
auto chosenWeight = GeneralUtils::GenerateRandomNumber<int32_t>(1, spawnWeight);
// Default to 0 incase something goes wrong in this calc
// Roll the spawner nodes based on their weights, higher weights = more likely to spawn
SpawnerNode* spawnNode = freeNodes[0];
for (auto* const node : freeNodes) {
chosenWeight -= node->weight;
if (chosenWeight <= 0) {
spawnNode = node;
break; // we rolled a spawner
}
}
++m_AmountSpawned; ++m_AmountSpawned;
m_EntityInfo.pos = spawnNode->position; m_EntityInfo.pos = spawnNode->position;
m_EntityInfo.rot = spawnNode->rotation; m_EntityInfo.rot = spawnNode->rotation;
@@ -93,26 +107,24 @@ Entity* Spawner::Spawn(std::vector<SpawnerNode*> freeNodes, const bool force) {
m_EntityInfo.spawnerID = m_Info.spawnerID; m_EntityInfo.spawnerID = m_Info.spawnerID;
} }
Entity* rezdE = Game::entityManager->CreateEntity(m_EntityInfo, nullptr); spawnedEntity = Game::entityManager->CreateEntity(m_EntityInfo, nullptr);
rezdE->GetGroups() = m_Info.groups; spawnedEntity->GetGroups() = m_Info.groups;
Game::entityManager->ConstructEntity(rezdE); Game::entityManager->ConstructEntity(spawnedEntity);
m_Entities.insert({ rezdE->GetObjectID(), spawnNode }); m_Entities[spawnedEntity->GetObjectID()] = spawnNode;
spawnNode->entities.push_back(rezdE->GetObjectID()); spawnNode->entities.push_back(spawnedEntity->GetObjectID());
if (m_Entities.size() == m_Info.amountMaintained) { if (m_Entities.size() == m_Info.amountMaintained) {
m_NeedsUpdate = false; m_NeedsUpdate = false;
} }
for (const auto& cb : m_EntitySpawnedCallbacks) { for (const auto& cb : m_EntitySpawnedCallbacks) {
cb(rezdE); cb(spawnedEntity);
} }
return rezdE;
} }
return nullptr; return spawnedEntity;
} }
void Spawner::AddSpawnedEntityDieCallback(std::function<void()> callback) { void Spawner::AddSpawnedEntityDieCallback(std::function<void()> callback) {
@@ -148,18 +160,18 @@ void Spawner::SoftReset() {
m_NeedsUpdate = true; m_NeedsUpdate = true;
} }
void Spawner::SetRespawnTime(float time) { void Spawner::SetRespawnTime(const float time) {
m_Info.respawnTime = time; m_Info.respawnTime = time;
for (size_t i = 0; i < m_WaitTimes.size(); ++i) { for (size_t i = 0; i < m_WaitTimes.size(); ++i) {
m_WaitTimes[i] = 0; m_WaitTimes[i] = 0;
}; }
m_Start = true; m_Start = true;
m_NeedsUpdate = true; m_NeedsUpdate = true;
} }
void Spawner::SetNumToMaintain(int32_t value) { void Spawner::SetNumToMaintain(const int32_t value) {
m_Info.amountMaintained = value; m_Info.amountMaintained = value;
} }
@@ -177,15 +189,8 @@ void Spawner::Update(const float deltaTime) {
return; return;
} }
if (!m_NeedsUpdate) return; if (!m_NeedsUpdate || !m_Active || m_Info.spawnsOnSmash) return;
if (!m_Active) return;
//if (m_Info.noTimedSpawn) return;
if (m_Info.spawnsOnSmash) {
if (!m_SpawnSmashFoundGroup) {
}
return;
}
for (size_t i = 0; i < m_WaitTimes.size(); ) { for (size_t i = 0; i < m_WaitTimes.size(); ) {
m_WaitTimes[i] += deltaTime; m_WaitTimes[i] += deltaTime;
if (m_WaitTimes[i] >= m_Info.respawnTime) { if (m_WaitTimes[i] >= m_Info.respawnTime) {
@@ -198,22 +203,21 @@ void Spawner::Update(const float deltaTime) {
} }
} }
std::vector<LWOOBJID> Spawner::GetSpawnedObjectIDs() const { const std::vector<LWOOBJID>& Spawner::GetSpawnedObjectIDs() const {
std::vector<LWOOBJID> ids; std::vector<LWOOBJID> ids;
ids.reserve(m_Entities.size()); ids.reserve(m_Entities.size());
for (const auto& [objId, spawnerNode] : m_Entities) { for (const auto objId : m_Entities | std::views::keys) {
ids.push_back(objId); ids.push_back(objId);
} }
return ids; return ids;
} }
void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) { void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) {
for (std::function<void()> cb : m_SpawnedEntityDieCallbacks) { for (const auto& cb : m_SpawnedEntityDieCallbacks) {
cb(); cb();
} }
m_NeedsUpdate = true; m_NeedsUpdate = true;
//m_RespawnTime = 10.0f;
m_WaitTimes.push_back(0.0f); m_WaitTimes.push_back(0.0f);
SpawnerNode* node; SpawnerNode* node;
@@ -221,9 +225,7 @@ void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) {
if (it != m_Entities.end()) node = it->second; if (it != m_Entities.end()) node = it->second;
else return; else return;
if (!node) { if (!node) return;
return;
}
for (size_t i = 0; i < node->entities.size();) { for (size_t i = 0; i < node->entities.size();) {
if (node->entities[i] && node->entities[i] == objectID) if (node->entities[i] && node->entities[i] == objectID)
@@ -249,6 +251,6 @@ void Spawner::Activate() {
} }
} }
void Spawner::SetSpawnLot(LOT lot) { void Spawner::SetSpawnLot(const LOT lot) {
m_EntityInfo.lot = lot; m_EntityInfo.lot = lot;
} }

View File

@@ -11,12 +11,41 @@
#include "LDFFormat.h" #include "LDFFormat.h"
#include "EntityInfo.h" #include "EntityInfo.h"
/**
* Any given spawner owns a certain number of spawner nodes
* these nodes are where entities are actually spawned
* The first spawner nodes waypoint in any given network contains the base config for all the spawner nodes
* Then each spawner node after the first may contain duplicate settings which override the base ones
* If spawner node 1 has an attached_path of "1", then all spawner nodes in this spawner network will have
* an attached_path of "1".
* Each spawner node can also specify attached_path of any other value and it will override the one provided by node 1.
* If a spawner node does NOT provide an override, the first one will be used
* I have no clue why the nodes are pointers, beats me
* sn = SpawnerNode
* Spawner
* ----------------
* | sn |
* | sn |
* | sn |
* | |
* | sn |
* | sn |
* -----------------
*/
struct SpawnerNode { struct SpawnerNode {
// This spawner nodes position in the world
NiPoint3 position = NiPoint3Constant::ZERO; NiPoint3 position = NiPoint3Constant::ZERO;
// The rotation of this spawner in the world
NiQuaternion rotation = QuatUtils::IDENTITY; NiQuaternion rotation = QuatUtils::IDENTITY;
// This spawners nodes ID in this spawner network
uint32_t nodeID = 0; uint32_t nodeID = 0;
// The max number of entities that can be spawned by this node
uint32_t nodeMax = 1; uint32_t nodeMax = 1;
// The weight (chance) this spawner node has. Higher is more common
int32_t weight = 1;
// The IDs of entities spawned by this spawner node
std::vector<LWOOBJID> entities; std::vector<LWOOBJID> entities;
// The config of all entities spawned by this node
LwoNameValue config; LwoNameValue config;
}; };
@@ -45,11 +74,10 @@ struct SpawnerInfo {
class Spawner { class Spawner {
public: public:
Spawner(SpawnerInfo info); Spawner(const SpawnerInfo& info);
~Spawner();
Entity* Spawn(); Entity* Spawn();
Entity* Spawn(std::vector<SpawnerNode*> freeNodes, bool force = false); Entity* Spawn(const std::vector<SpawnerNode*>& freeNodes, bool force = false);
void Update(float deltaTime); void Update(float deltaTime);
void NotifyOfEntityDeath(const LWOOBJID& objectID); void NotifyOfEntityDeath(const LWOOBJID& objectID);
void Activate(); void Activate();
@@ -57,16 +85,16 @@ public:
int32_t GetAmountSpawned() { return m_AmountSpawned; }; int32_t GetAmountSpawned() { return m_AmountSpawned; };
std::string GetName() { return m_Info.name; }; std::string GetName() { return m_Info.name; };
std::vector<std::string> GetGroups() { return m_Info.groups; }; std::vector<std::string> GetGroups() { return m_Info.groups; };
void AddSpawnedEntityDieCallback(std::function<void()> callback); void AddSpawnedEntityDieCallback(const std::function<void()> callback);
void AddEntitySpawnedCallback(std::function<void(Entity*)> callback); void AddEntitySpawnedCallback(const std::function<void(Entity*)> callback);
void SetSpawnLot(LOT lot); void SetSpawnLot(const LOT lot);
void Reset(); void Reset();
void DestroyAllEntities(); void DestroyAllEntities();
void SoftReset(); void SoftReset();
void SetRespawnTime(float time); void SetRespawnTime(const float time);
void SetNumToMaintain(int32_t value); void SetNumToMaintain(const int32_t value);
bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; }; bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; };
std::vector<LWOOBJID> GetSpawnedObjectIDs() const; const std::vector<LWOOBJID>& GetSpawnedObjectIDs() const;
SpawnerInfo m_Info; SpawnerInfo m_Info;
bool m_Active = true; bool m_Active = true;

View File

@@ -123,22 +123,24 @@ void Zone::LoadZoneIntoMemory() {
if (!data) continue; if (!data) continue;
if (data->GetKey() == u"spawner_node_id") { if (data->GetKey() == u"spawner_node_id") {
node->nodeID = std::stoi(data->GetValueAsString()); node->nodeID = GeneralUtils::TryParse(data->GetValueAsString(), 0);
} else if (data->GetKey() == u"spawner_max_per_node") { } else if (data->GetKey() == u"spawner_max_per_node") {
node->nodeMax = std::stoi(data->GetValueAsString()); node->nodeMax = GeneralUtils::TryParse(data->GetValueAsString(), 0);
} else if (data->GetKey() == u"groupID") { // Load object group } else if (data->GetKey() == u"groupID") { // Load object group
std::string groupStr = data->GetValueAsString(); info.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';');
info.groups = GeneralUtils::SplitString(groupStr, ';');
if (info.groups.back().empty()) info.groups.erase(info.groups.end() - 1); if (info.groups.back().empty()) info.groups.erase(info.groups.end() - 1);
} else if (data->GetKey() == u"grpNameQBShowBricks") { } else if (data->GetKey() == u"grpNameQBShowBricks") {
if (data->GetValueAsString().empty()) continue;
/*std::string groupStr = data->GetValueAsString();
info.groups.push_back(groupStr);*/
info.grpNameQBShowBricks = data->GetValueAsString(); info.grpNameQBShowBricks = data->GetValueAsString();
} else if (data->GetKey() == u"spawner_name") { } else if (data->GetKey() == u"spawner_name") {
info.name = data->GetValueAsString(); info.name = data->GetValueAsString();
} else if (data->GetKey() == u"weight") {
node->weight = GeneralUtils::TryParse(data->GetValueAsString(), 1);
if (node->weight <= 0) {
LOG("Found a spawner with a weight of <= 0, is this intentional? %s:%i", info.name.c_str(), node->nodeID);
}
} }
} }
info.nodes.push_back(node); info.nodes.push_back(node);
} }
info.templateID = path.spawner.spawnedLOT; info.templateID = path.spawner.spawnedLOT;