Compare commits

..

2 Commits

Author SHA1 Message Date
David Markowitz
47535f3c3a feedback 2026-06-17 01:56:18 -07:00
David Markowitz
105ddf4e1d feat: enemy npc pathing
they live 🎉
tested that enemies path all around the world should they have a path configured.
tested that the admiral in gf (at the first camp) paths now.
fixes #1546
2026-06-17 01:47:09 -07:00
16 changed files with 136 additions and 269 deletions

View File

@@ -105,7 +105,15 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
auto* team = TeamContainer::GetTeam(playerID); auto* team = TeamContainer::GetTeam(playerID);
if (team != nullptr) { if (team != nullptr) {
TeamContainer::RemoveMember(team, playerID, false, false, true); const auto memberName = GeneralUtils::UTF8ToUTF16(player.playerName);
for (const auto memberId : team->memberIDs) {
const auto& otherMember = GetPlayerData(memberId);
if (!otherMember) continue;
TeamContainer::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 });
}
} }
ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut); ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut);

View File

@@ -289,14 +289,6 @@ 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

@@ -38,11 +38,3 @@ std::vector<CDObjectSkills> CDObjectSkillsTable::Query(std::function<bool(CDObje
return data; return data;
} }
std::vector<CDObjectSkills> CDObjectSkillsTable::Get(const LOT lot) const {
std::vector<CDObjectSkills> toReturn;
for (const auto& entry : GetEntries()) {
if (entry.objectTemplate == lot) toReturn.push_back(entry);
}
return toReturn;
}

View File

@@ -4,13 +4,12 @@
#include "CDTable.h" #include "CDTable.h"
#include <cstdint> #include <cstdint>
#include <vector>
struct CDObjectSkills { struct CDObjectSkills {
uint32_t objectTemplate; //!< The LOT of the item uint32_t objectTemplate; //!< The LOT of the item
uint32_t skillID; //!< The Skill ID of the object uint32_t skillID; //!< The Skill ID of the object
uint32_t castOnType; //!< ??? uint32_t castOnType; //!< ???
int32_t AICombatWeight; //!< ??? uint32_t AICombatWeight; //!< ???
}; };
class CDObjectSkillsTable : public CDTable<CDObjectSkillsTable, std::vector<CDObjectSkills>> { class CDObjectSkillsTable : public CDTable<CDObjectSkillsTable, std::vector<CDObjectSkills>> {
@@ -18,6 +17,5 @@ public:
void LoadValuesFromDatabase(); void LoadValuesFromDatabase();
// Queries the table with a custom "where" clause // Queries the table with a custom "where" clause
std::vector<CDObjectSkills> Query(std::function<bool(CDObjectSkills)> predicate); std::vector<CDObjectSkills> Query(std::function<bool(CDObjectSkills)> predicate);
std::vector<CDObjectSkills> Get(const LOT lot) const;
}; };

View File

@@ -5,40 +5,20 @@
void NpcCombatSkillBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bit_stream, BehaviorBranchContext branch) { void NpcCombatSkillBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bit_stream, BehaviorBranchContext branch) {
context->skillTime = this->m_npcSkillTime; context->skillTime = this->m_npcSkillTime;
const auto* const targetEntity = Game::entityManager->GetEntity(branch.target);
const auto* const sourceEntity = Game::entityManager->GetEntity(context->caster);
bool cast = true;
// Check that the target is within the cast range
if (targetEntity && sourceEntity && this->m_maxRange != 0.0f) {
const auto targetPos = targetEntity->GetPosition();
const auto sourcePos = sourceEntity->GetPosition();
const auto distance = NiPoint3::DistanceSquared(targetPos, sourcePos);
cast = distance >= this->m_minRange && distance <= this->m_maxRange;
}
if (cast) {
for (auto* behavior : this->m_behaviors) { for (auto* behavior : this->m_behaviors) {
behavior->Calculate(context, bit_stream, branch); behavior->Calculate(context, bit_stream, branch);
} }
} else {
// We failed to find a valid target, do not continue the behavior
context->foundTarget = false;
}
} }
void NpcCombatSkillBehavior::Load() { void NpcCombatSkillBehavior::Load() {
this->m_npcSkillTime = GetFloat("npc skill time"); this->m_npcSkillTime = GetFloat("npc skill time");
this->m_minRange = GetFloat("min range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements
this->m_minRange *= this->m_minRange;
this->m_maxRange = GetFloat("max range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements
this->m_maxRange *= this->m_maxRange;
const auto parameters = GetParameterNames(); const auto parameters = GetParameterNames();
for (const auto& [parameter, value] : parameters) { for (const auto& parameter : parameters) {
if (parameter.rfind("behavior", 0) == 0) { if (parameter.first.rfind("behavior", 0) == 0) {
auto* action = GetAction(value); auto* action = GetAction(parameter.second);
this->m_behaviors.push_back(action); this->m_behaviors.push_back(action);
} }

View File

@@ -9,7 +9,6 @@ public:
float m_npcSkillTime; float m_npcSkillTime;
float m_maxRange{}; float m_maxRange{};
float m_minRange{};
/* /*
* Inherited * Inherited

View File

@@ -13,8 +13,6 @@
#include "CDClientDatabase.h" #include "CDClientDatabase.h"
#include "CDClientManager.h" #include "CDClientManager.h"
#include "CDObjectSkillsTable.h"
#include "CDSkillBehaviorTable.h"
#include "DestroyableComponent.h" #include "DestroyableComponent.h"
#include <algorithm> #include <algorithm>
@@ -45,7 +43,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
//Grab the aggro information from BaseCombatAI: //Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt( auto componentQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius, minRoundLength, maxRoundLength, combatRoundLength FROM BaseCombatAIComponent WHERE id = ?;"); "SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;");
componentQuery.bind(1, static_cast<int>(componentID)); componentQuery.bind(1, static_cast<int>(componentID));
auto componentResult = componentQuery.execQuery(); auto componentResult = componentQuery.execQuery();
@@ -65,37 +63,44 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
if (!componentResult.fieldIsNull("hardTetherRadius")) if (!componentResult.fieldIsNull("hardTetherRadius"))
m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius"); m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius");
m_MinRoundLength = componentResult.getFloatField("minRoundLength");
m_MaxRoundLength = componentResult.getFloatField("maxRoundLength");
m_CombatRoundLength = componentResult.getFloatField("combatRoundLength");
} }
componentResult.finalize();
// Get aggro and tether radius from settings and use this if it is present. Only overwrite the // Get aggro and tether radius from settings and use this if it is present. Only overwrite the
// radii if it is greater than the one in the database. // radii if it is greater than the one in the database.
m_AggroRadius = m_Parent->HasVar(u"aggroRadius") ? m_Parent->GetVar<float>(u"aggroRadius") : m_AggroRadius; if (m_Parent) {
m_HardTetherRadius = m_Parent->HasVar(u"tetherRadius") ? m_Parent->GetVar<float>(u"tetherRadius") : m_HardTetherRadius; auto aggroRadius = m_Parent->GetVar<float>(u"aggroRadius");
m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius;
auto tetherRadius = m_Parent->GetVar<float>(u"tetherRadius");
m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius;
}
/* /*
* Find skills * Find skills
*/ */
for (const auto objectSkill : CDClientManager::GetTable<CDObjectSkillsTable>()->Get(parent->GetLOT())) { auto skillQuery = CDClientDatabase::CreatePreppedStmt(
const auto skillBehavior = CDClientManager::GetTable<CDSkillBehaviorTable>()->GetSkillByID(objectSkill.skillID); "SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);");
if (skillBehavior.skillID == objectSkill.skillID) { skillQuery.bind(1, static_cast<int>(parent->GetLOT()));
const auto skillId = skillBehavior.skillID;
const auto abilityCooldown = skillBehavior.cooldown; auto result = skillQuery.execQuery();
const auto behaviorId = skillBehavior.behaviorID; while (!result.eof()) {
const auto skillId = static_cast<uint32_t>(result.getIntField("skillID"));
const auto combatWeight = objectSkill.AICombatWeight; const auto abilityCooldown = static_cast<float>(result.getFloatField("cooldown"));
const auto behaviorId = static_cast<uint32_t>(result.getIntField("behaviorID"));
auto* behavior = Behavior::CreateBehavior(behaviorId); auto* behavior = Behavior::CreateBehavior(behaviorId);
AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight }; std::stringstream behaviorQuery;
AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior };
m_SkillEntries.push_back(entry); m_SkillEntries.push_back(entry);
}
result.nextRow();
} }
Stun(1.0f); Stun(1.0f);
@@ -243,12 +248,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
bool hasSkillToCast = false; bool hasSkillToCast = false;
int32_t maxSkillWeights = 0;
for (auto& entry : m_SkillEntries) { for (auto& entry : m_SkillEntries) {
if (entry.cooldown > 0.0f) { if (entry.cooldown > 0.0f) {
entry.cooldown -= deltaTime; entry.cooldown -= deltaTime;
} else { } else {
maxSkillWeights += entry.combatWeight;
hasSkillToCast = true; hasSkillToCast = true;
} }
} }
@@ -334,23 +337,14 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
LookAt(target->GetPosition()); LookAt(target->GetPosition());
} }
// Roll to find which skill we'll try to cast for (auto i = 0; i < m_SkillEntries.size(); ++i) {
auto randomizedWeight = GeneralUtils::GenerateRandomNumber<int32_t>(0, maxSkillWeights); auto entry = m_SkillEntries.at(i);
for (auto& entry : m_SkillEntries) { if (entry.cooldown > 0) {
// Skill isn't cooled off yet
if (entry.cooldown > 0.0f) {
continue; continue;
} }
randomizedWeight -= entry.combatWeight; const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, LWOOBJID_EMPTY);
// if the weight is still greater than 0 continue to the next rolled skill
if (randomizedWeight > 0) {
continue;
}
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, GetTarget());
if (result.success) { if (result.success) {
if (m_MovementAI != nullptr) { if (m_MovementAI != nullptr) {
@@ -365,6 +359,8 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
entry.cooldown = entry.abilityCooldown + m_SkillTime; entry.cooldown = entry.abilityCooldown + m_SkillTime;
m_SkillEntries[i] = entry;
break; break;
} }
} }
@@ -759,8 +755,8 @@ void BaseCombatAIComponent::SetTetherSpeed(float value) {
m_TetherSpeed = value; m_TetherSpeed = value;
} }
void BaseCombatAIComponent::Stun(const float time, const bool force) { void BaseCombatAIComponent::Stun(const float time) {
if (!force && (m_StunImmune || m_StunTime > time)) { if (m_StunImmune || m_StunTime > time) {
return; return;
} }
@@ -918,16 +914,8 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport
} }
auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats"); auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats");
for (const auto& [id, threat] : m_RemovedThreatList) { for (const auto& [id, threat] : m_ThreatEntries) {
ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat; ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat;
} }
auto& skillInfo = cmptType.PushDebug("Skill Info");
for (const auto& skill : m_SkillEntries) {
auto& skillDebug = skillInfo.PushDebug("Skill ID " + std::to_string(skill.skillId));
skillDebug.PushDebug<AMFDoubleValue>("Cooldown") = skill.cooldown;
skillDebug.PushDebug<AMFDoubleValue>("Ability Cooldown") = skill.abilityCooldown;
skillDebug.PushDebug<AMFIntValue>("AI Combat Weight") = skill.combatWeight;
}
return true; return true;
} }

View File

@@ -33,15 +33,13 @@ enum class AiState : uint32_t {
*/ */
struct AiSkillEntry struct AiSkillEntry
{ {
uint32_t skillId{}; uint32_t skillId;
float cooldown{}; float cooldown;
float abilityCooldown{}; float abilityCooldown;
Behavior* behavior{}; Behavior* behavior;
int32_t combatWeight{};
}; };
/** /**
@@ -183,9 +181,8 @@ public:
/** /**
* Stuns the entity for a certain amount of time, will not work if the entity is stun immune * Stuns the entity for a certain amount of time, will not work if the entity is stun immune
* @param time the time to stun the entity, if stunnable * @param time the time to stun the entity, if stunnable
* @param force whether or not to force the stun and ignore checks
*/ */
void Stun(float time, const bool force = false); void Stun(float time);
/** /**
* Gets the radius that will cause this entity to get aggro'd, causing a target chase * Gets the radius that will cause this entity to get aggro'd, causing a target chase
@@ -399,17 +396,9 @@ private:
*/ */
bool m_DirtyStateOrTarget = false; bool m_DirtyStateOrTarget = false;
// Min amount of time to remain as in combat after casting a skill
float m_MinRoundLength = 0.0f;
// max amount of time to remain as in combat after casting a skill
float m_MaxRoundLength = 0.0f;
// The amount of time the entity will be forced to tether for // The amount of time the entity will be forced to tether for
float m_ForcedTetherTime = 0.0f; float m_ForcedTetherTime = 0.0f;
float m_CombatRoundLength = 0.0f;
// The amount of time a removed threat will be ignored for. // The amount of time a removed threat will be ignored for.
std::map<LWOOBJID, float> m_RemovedThreatList; std::map<LWOOBJID, float> m_RemovedThreatList;

View File

@@ -37,6 +37,8 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component
m_Info = info; m_Info = info;
m_AtFinalWaypoint = true; m_AtFinalWaypoint = true;
m_BaseCombatAI = nullptr;
m_BaseCombatAI = m_Parent->GetComponent<BaseCombatAIComponent>(); m_BaseCombatAI = m_Parent->GetComponent<BaseCombatAIComponent>();
//Try and fix the insane values: //Try and fix the insane values:
@@ -129,11 +131,7 @@ void MovementAIComponent::Update(const float deltaTime) {
m_TimeTravelled += deltaTime; m_TimeTravelled += deltaTime;
const auto approxPos = ApproximateLocation(); SetPosition(ApproximateLocation());
SetPosition(approxPos);
// Set the AIs new home based on where our current waypoint is IF we're idle, that way we can return to this
// when resuming the pathing after losing aggro while moving the aggro hitbox with us
if (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle) m_BaseCombatAI->SetStartingPosition(approxPos);
if (m_TimeTravelled < m_TimeToTravel) return; if (m_TimeTravelled < m_TimeToTravel) return;
m_TimeTravelled = 0.0f; m_TimeTravelled = 0.0f;
@@ -168,8 +166,6 @@ void MovementAIComponent::Update(const float deltaTime) {
SetRotation(QuatUtils::LookAt(source, m_NextWaypoint)); SetRotation(QuatUtils::LookAt(source, m_NextWaypoint));
} }
} else { } else {
// Only try to renew or continue the path if we're in the idle or spawn state and we actually have a combatAI component
if (!m_BaseCombatAI || (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle)) {
// Check if there are more waypoints in the queue, if so set our next destination to the next waypoint // Check if there are more waypoints in the queue, if so set our next destination to the next waypoint
const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1; const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1;
RunWaypointCommands(waypointNum); RunWaypointCommands(waypointNum);
@@ -184,6 +180,7 @@ void MovementAIComponent::Update(const float deltaTime) {
SetPath(waypoints); SetPath(waypoints);
} else if (m_Path->pathBehavior == PathBehavior::Once) { } else if (m_Path->pathBehavior == PathBehavior::Once) {
// In this case we intended to follow a path and once we've followed it we camp there, otherwise we'd just wander home again. // In this case we intended to follow a path and once we've followed it we camp there, otherwise we'd just wander home again.
if (m_BaseCombatAI) m_BaseCombatAI->SetStartingPosition(m_SourcePosition);
Stop(); Stop();
return; return;
} }
@@ -197,7 +194,6 @@ void MovementAIComponent::Update(const float deltaTime) {
m_CurrentPath.pop(); m_CurrentPath.pop();
} }
} }
}
Game::entityManager->SerializeEntity(m_Parent); Game::entityManager->SerializeEntity(m_Parent);
} }
@@ -222,7 +218,8 @@ NiPoint3 MovementAIComponent::GetCurrentWaypoint() const {
NiPoint3 MovementAIComponent::ApproximateLocation() const { NiPoint3 MovementAIComponent::ApproximateLocation() const {
auto source = m_SourcePosition; auto source = m_SourcePosition;
if (AtFinalWaypoint()) return m_Parent->GetPosition();
if (AtFinalWaypoint()) return source;
auto destination = m_NextWaypoint; auto destination = m_NextWaypoint;
@@ -524,7 +521,6 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf
movementInfo.PushDebug<AMFBoolValue>("Lock Rotation") = m_LockRotation; movementInfo.PushDebug<AMFBoolValue>("Lock Rotation") = m_LockRotation;
movementInfo.PushDebug<AMFBoolValue>("Paused") = m_Paused; movementInfo.PushDebug<AMFBoolValue>("Paused") = m_Paused;
movementInfo.PushDebug<AMFDoubleValue>("Pulling To Point") = m_PullingToPoint; movementInfo.PushDebug<AMFDoubleValue>("Pulling To Point") = m_PullingToPoint;
movementInfo.PushDebug<AMFBoolValue>("At Final Waypoint") = m_AtFinalWaypoint;
auto& pullPointInfo = movementInfo.PushDebug("Pull Point"); auto& pullPointInfo = movementInfo.PushDebug("Pull Point");
pullPointInfo.PushDebug<AMFDoubleValue>("X") = m_PullPoint.x; pullPointInfo.PushDebug<AMFDoubleValue>("X") = m_PullPoint.x;

View File

@@ -16,7 +16,6 @@
#include "eReplicaComponentType.h" #include "eReplicaComponentType.h"
#include "RenderComponent.h" #include "RenderComponent.h"
#include "PlayerManager.h" #include "PlayerManager.h"
#include "eStateChangeType.h"
#include <vector> #include <vector>
@@ -49,30 +48,10 @@ void BossSpiderQueenEnemyServer::OnStartup(Entity* self) {
combat->SetStunImmune(true); combat->SetStunImmune(true);
m_CurrentBossStage = 1; m_CurrentBossStage = 1;
ToggleAttacking(*self, false);
self->SetProximityRadius(65.0f, "AggroRadius");
// Obtain faction and collision group to save for subsequent resets // Obtain faction and collision group to save for subsequent resets
} }
void BossSpiderQueenEnemyServer::OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) {
if (name != "AggroRadius" || !entering || !entering->IsPlayer()) return;
auto playerCount = self->GetVar<int32_t>(u"player_count");
if (status == "ENTER") {
if (playerCount == 0) {
ToggleAttacking(*self, true);
}
playerCount++;
} else if (status == "LEAVE") {
playerCount--;
if (playerCount == 0) {
ToggleAttacking(*self, false);
}
}
self->SetVar<int32_t>(u"player_count", playerCount);
}
void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) {
if (Game::zoneManager->GetZoneID().GetMapID() == instanceZoneID && killer) { if (Game::zoneManager->GetZoneID().GetMapID() == instanceZoneID && killer) {
for (const auto& player : PlayerManager::GetAllPlayers()) { for (const auto& player : PlayerManager::GetAllPlayers()) {
@@ -92,7 +71,6 @@ void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) {
self->SetPosition({ 10000, 0, 10000 }); self->SetPosition({ 10000, 0, 10000 });
Game::entityManager->SerializeEntity(self); Game::entityManager->SerializeEntity(self);
ToggleAttacking(*self, false);
controller->OnFireEventServerSide(self, "ClearProperty"); controller->OnFireEventServerSide(self, "ClearProperty");
} }
@@ -656,19 +634,3 @@ float BossSpiderQueenEnemyServer::PlayAnimAndReturnTime(Entity* self, const std:
return animTimer; return animTimer;
} }
void BossSpiderQueenEnemyServer::ToggleAttacking(Entity& self, bool on) {
const auto stoppedFlag = self.GetVarAs<bool>(u"stoppedFlag");
if (!on) {
if (stoppedFlag) return;
self.SetVar(u"stoppedFlag", true);
combat->Stun(100000.0f, true); // forcibly stun so we stop attacking people trying to put on armor
} else {
if (!stoppedFlag) return;
self.SetVar(u"stoppedFlag", false);
combat->Stun(0.0f, true); // forcibly turn off the stun we put on above
}
}

View File

@@ -46,10 +46,7 @@ public:
void OnTimerDone(Entity* self, std::string timerName) override; void OnTimerDone(Entity* self, std::string timerName) override;
void OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status);
private: private:
void ToggleAttacking(Entity& self, bool on);
//Regular variables: //Regular variables:
DestroyableComponent* destroyable = nullptr; DestroyableComponent* destroyable = nullptr;
ControllablePhysicsComponent* controllable = nullptr; ControllablePhysicsComponent* controllable = nullptr;

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) {
const auto& spawnedObjIds = spawner->GetSpawnedObjectIDs(); 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

@@ -1019,7 +1019,6 @@ void HandlePacket(Packet* packet) {
if (user) { if (user) {
Character* c = user->GetLastUsedChar(); Character* c = user->GetLastUsedChar();
if (c != nullptr) { if (c != nullptr) {
if (Game::entityManager->GetEntity(c->GetObjectID())) return;
std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName()); std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName());
Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress); Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress);

View File

@@ -6,10 +6,8 @@
#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();
@@ -64,6 +62,10 @@ 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) {
@@ -75,25 +77,9 @@ Entity* Spawner::Spawn() {
return Spawn(freeNodes); return Spawn(freeNodes);
} }
Entity* Spawner::Spawn(const std::vector<SpawnerNode*>& freeNodes, const bool force) { Entity* Spawner::Spawn(std::vector<SpawnerNode*> freeNodes, const bool force) {
Entity* spawnedEntity = nullptr; if (force || ((m_Entities.size() < m_Info.amountMaintained) && (freeNodes.size() > 0) && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) {
if (force || ((m_Entities.size() < m_Info.amountMaintained) && !freeNodes.empty() && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) { SpawnerNode* spawnNode = freeNodes[GeneralUtils::GenerateRandomNumber<int>(0, freeNodes.size() - 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;
@@ -107,24 +93,26 @@ Entity* Spawner::Spawn(const std::vector<SpawnerNode*>& freeNodes, const bool fo
m_EntityInfo.spawnerID = m_Info.spawnerID; m_EntityInfo.spawnerID = m_Info.spawnerID;
} }
spawnedEntity = Game::entityManager->CreateEntity(m_EntityInfo, nullptr); Entity* rezdE = Game::entityManager->CreateEntity(m_EntityInfo, nullptr);
spawnedEntity->GetGroups() = m_Info.groups; rezdE->GetGroups() = m_Info.groups;
Game::entityManager->ConstructEntity(spawnedEntity); Game::entityManager->ConstructEntity(rezdE);
m_Entities[spawnedEntity->GetObjectID()] = spawnNode; m_Entities.insert({ rezdE->GetObjectID(), spawnNode });
spawnNode->entities.push_back(spawnedEntity->GetObjectID()); spawnNode->entities.push_back(rezdE->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(spawnedEntity); cb(rezdE);
}
} }
return spawnedEntity; return rezdE;
}
return nullptr;
} }
void Spawner::AddSpawnedEntityDieCallback(std::function<void()> callback) { void Spawner::AddSpawnedEntityDieCallback(std::function<void()> callback) {
@@ -160,18 +148,18 @@ void Spawner::SoftReset() {
m_NeedsUpdate = true; m_NeedsUpdate = true;
} }
void Spawner::SetRespawnTime(const float time) { void Spawner::SetRespawnTime(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(const int32_t value) { void Spawner::SetNumToMaintain(int32_t value) {
m_Info.amountMaintained = value; m_Info.amountMaintained = value;
} }
@@ -189,8 +177,15 @@ void Spawner::Update(const float deltaTime) {
return; return;
} }
if (!m_NeedsUpdate || !m_Active || m_Info.spawnsOnSmash) return; if (!m_NeedsUpdate) 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) {
@@ -203,21 +198,22 @@ void Spawner::Update(const float deltaTime) {
} }
} }
const std::vector<LWOOBJID> Spawner::GetSpawnedObjectIDs() 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 : m_Entities | std::views::keys) { for (const auto& [objId, spawnerNode] : m_Entities) {
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 (const auto& cb : m_SpawnedEntityDieCallbacks) { for (std::function<void()> 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;
@@ -225,7 +221,9 @@ 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) return; if (!node) {
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)
@@ -251,6 +249,6 @@ void Spawner::Activate() {
} }
} }
void Spawner::SetSpawnLot(const LOT lot) { void Spawner::SetSpawnLot(LOT lot) {
m_EntityInfo.lot = lot; m_EntityInfo.lot = lot;
} }

View File

@@ -11,41 +11,12 @@
#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;
}; };
@@ -74,10 +45,11 @@ struct SpawnerInfo {
class Spawner { class Spawner {
public: public:
Spawner(const SpawnerInfo& info); Spawner(SpawnerInfo info);
~Spawner();
Entity* Spawn(); Entity* Spawn();
Entity* Spawn(const std::vector<SpawnerNode*>& freeNodes, bool force = false); Entity* Spawn(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();
@@ -85,16 +57,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(const std::function<void()> callback); void AddSpawnedEntityDieCallback(std::function<void()> callback);
void AddEntitySpawnedCallback(const std::function<void(Entity*)> callback); void AddEntitySpawnedCallback(std::function<void(Entity*)> callback);
void SetSpawnLot(const LOT lot); void SetSpawnLot(LOT lot);
void Reset(); void Reset();
void DestroyAllEntities(); void DestroyAllEntities();
void SoftReset(); void SoftReset();
void SetRespawnTime(const float time); void SetRespawnTime(float time);
void SetNumToMaintain(const int32_t value); void SetNumToMaintain(int32_t value);
bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; }; bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; };
const std::vector<LWOOBJID> GetSpawnedObjectIDs() const; std::vector<LWOOBJID> GetSpawnedObjectIDs() const;
SpawnerInfo m_Info; SpawnerInfo m_Info;
bool m_Active = true; bool m_Active = true;

View File

@@ -123,25 +123,22 @@ 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 = GeneralUtils::TryParse(data->GetValueAsString(), 0); node->nodeID = std::stoi(data->GetValueAsString());
} else if (data->GetKey() == u"spawner_max_per_node") { } else if (data->GetKey() == u"spawner_max_per_node") {
node->nodeMax = GeneralUtils::TryParse(data->GetValueAsString(), 0); node->nodeMax = std::stoi(data->GetValueAsString());
} else if (data->GetKey() == u"groupID") { // Load object group } else if (data->GetKey() == u"groupID") { // Load object group
info.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';'); std::string groupStr = 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);
node->weight = 1;
} }
} }
}
info.nodes.push_back(node); info.nodes.push_back(node);
} }
info.templateID = path.spawner.spawnedLOT; info.templateID = path.spawner.spawnedLOT;