diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index 148cab2ad..56fc8974f 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -105,15 +105,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { auto* team = TeamContainer::GetTeam(playerID); if (team != nullptr) { - 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 }); - } + TeamContainer::RemoveMember(team, playerID, false, false, true); } ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut); diff --git a/dCommon/Amf3.h b/dCommon/Amf3.h index 174ad814d..dce06973e 100644 --- a/dCommon/Amf3.h +++ b/dCommon/Amf3.h @@ -4,7 +4,6 @@ #include "dCommonVars.h" #include "Logger.h" #include "Game.h" - #include #include #include @@ -368,9 +367,37 @@ class AMFArrayValue : public AMFBaseValue { } template - AmfType& PushDebug(const std::string_view name) { + AmfType& PushDebug(const std::string_view name, const std::string& objectType = "", const uint32_t warningLevel = 0) { + size_t i = 0; + for (; i < m_Dense.size(); i++) { + const auto& cast = dynamic_cast(m_Dense[i].get()); + if (!cast) continue; + + const auto& nameValue = cast->Get("name"); + if (!nameValue || nameValue->GetValue() != name) continue; + + if (!objectType.empty()) { + cast->Insert("type", objectType); + } + + if (warningLevel != 0) { + cast->Insert("warningLevel", warningLevel); + } + + // found a duplicate, return this instead + auto valueCast = dynamic_cast(cast->Get("value")); + if (valueCast) return *valueCast; + } + auto* value = PushArray(); - value->Insert("name", name.data()); + value->Insert("name", name.data()); + if (!objectType.empty()) { + value->Insert("type", objectType); + } + + if (warningLevel != 0) { + value->Insert("warningLevel", warningLevel); + } return value->Insert("value", std::make_unique()); } diff --git a/dCommon/LDFFormat.h b/dCommon/LDFFormat.h index 81a6648c6..0f515d302 100644 --- a/dCommon/LDFFormat.h +++ b/dCommon/LDFFormat.h @@ -289,6 +289,14 @@ struct LwoNameValue { 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(const LwoNameValue& other) { diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp index a07446b5a..522c3e884 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp @@ -38,3 +38,11 @@ std::vector CDObjectSkillsTable::Query(std::function CDObjectSkillsTable::Get(const LOT lot) const { + std::vector toReturn; + for (const auto& entry : GetEntries()) { + if (entry.objectTemplate == lot) toReturn.push_back(entry); + } + return toReturn; +} diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h index 731f6657b..ed3142128 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h @@ -4,12 +4,13 @@ #include "CDTable.h" #include +#include struct CDObjectSkills { uint32_t objectTemplate; //!< The LOT of the item uint32_t skillID; //!< The Skill ID of the object uint32_t castOnType; //!< ??? - uint32_t AICombatWeight; //!< ??? + int32_t AICombatWeight; //!< ??? }; class CDObjectSkillsTable : public CDTable> { @@ -17,5 +18,6 @@ class CDObjectSkillsTable : public CDTable Query(std::function predicate); + std::vector Get(const LOT lot) const; }; diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index c42654709..ab1589d53 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -316,15 +316,16 @@ void Entity::Initialize() { controllablePhysics->LoadFromXml(m_Character->GetXMLDoc()); const auto mapID = Game::server->GetZoneID(); + const auto& targetSceneName = m_Character->GetTargetScene(); //If we came from another zone, put us in the starting loc - if (m_Character->GetZoneID() != Game::server->GetZoneID() || mapID == 1603) { // Exception for Moon Base as you tend to spawn on the roof. + // Exception for Moon Base as you tend to spawn on the roof. + // second exception if we have a specified targetScene since that would only be possible in a test map + if (m_Character->GetZoneID() != Game::server->GetZoneID() || mapID == 1603 || !targetSceneName.empty()) { NiPoint3 pos; NiQuaternion rot = QuatUtils::IDENTITY; - const auto& targetSceneName = m_Character->GetTargetScene(); auto* targetScene = Game::entityManager->GetSpawnPointEntity(targetSceneName); - if (m_Character->HasBeenToWorld(mapID) && targetSceneName.empty()) { pos = m_Character->GetRespawnPoint(mapID); rot = Game::zoneManager->GetZone()->GetSpawnRot(); @@ -612,14 +613,15 @@ void Entity::Initialize() { if (rebuildResetTime != 0.0f) { quickBuildComponent->SetResetTime(rebuildResetTime); - - // Known bug with moving platform in FV that casues it to build at the end instead of the start. - // This extends the smash time so players can ride up the lift. - if (m_TemplateID == 9483) { - quickBuildComponent->SetResetTime(quickBuildComponent->GetResetTime() + 25); - } } + const auto objectID = GetObjectID(); + // FV tree handler for when built so it sets the state to moving at the correct time + if (GetLOT() == 9483) quickBuildComponent->AddQuickBuildCompleteCallback([objectID](Entity* user) { + auto* const entity = Game::entityManager->GetEntity(objectID); + if (entity) GameMessages::SendPlatformResync(entity, UNASSIGNED_SYSTEM_ADDRESS, false, 0, 1, 1, eMovementPlatformState::Moving, true); + }); + const auto activityID = GetVar(u"activityID"); if (activityID > 0) { @@ -1017,6 +1019,7 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke const bool hasParent = m_ParentEntity != nullptr || m_SpawnerID != 0; outBitStream.Write(hasParent); if (hasParent) { + // 触るな! if (m_ParentEntity != nullptr) outBitStream.Write(GeneralUtils::SetBit(m_ParentEntity->GetObjectID(), static_cast(eObjectBits::CLIENT))); else if (m_Spawner != nullptr && m_Spawner->m_Info.isNetwork) outBitStream.Write(m_SpawnerID); else outBitStream.Write(GeneralUtils::SetBit(m_SpawnerID, static_cast(eObjectBits::CLIENT))); @@ -1615,7 +1618,7 @@ void Entity::Kill(Entity* murderer, const eKillType killType) { const auto& grpNameQBShowBricks = GetVarAsString(u"grpNameQBShowBricks"); if (!grpNameQBShowBricks.empty()) { - for (auto* const spawner : Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks)) if (spawner) spawner->Spawn(); + for (auto* const spawner : Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks)) if (spawner) spawner->Spawn(); for (auto* const spawner : Game::zoneManager->GetSpawnersInGroup(grpNameQBShowBricks)) if (spawner) spawner->Spawn(); } @@ -2233,10 +2236,16 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& r const auto& objTableInfo = table->GetByID(GetLOT()); - objectInfo.PushDebug("Name") = objTableInfo.name; - objectInfo.PushDebug("Template ID(LOT)") = GetLOT(); - objectInfo.PushDebug("Object ID") = std::to_string(GetObjectID()); - objectInfo.PushDebug("Spawner's Object ID") = std::to_string(GetSpawnerID()); + objectInfo.PushDebug("Name", "name") = objTableInfo.name; + objectInfo.PushDebug("Template ID(LOT)", "LOT") = GetLOT(); + objectInfo.PushDebug("Object ID", "LWOOBJID") = std::to_string(GetObjectID()); + objectInfo.PushDebug("Spawner's Object ID", "LWOOBJID") = std::to_string(GetSpawnerID()); + objectInfo.PushDebug("Owner override", "LWOOBJID") = std::to_string(m_OwnerOverride); + auto& children = objectInfo.PushDebug("Child Objects"); + int i = 1; + for (const auto* child : m_ChildEntities) { + if (child) children.PushDebug("Child " + std::to_string(i++), "LWOOBJID") = std::to_string(child->GetObjectID()); + } auto& componentDetails = objectInfo.PushDebug("Component Information"); for (const auto [id, component] : m_Components) { diff --git a/dGame/dBehaviors/NpcCombatSkillBehavior.cpp b/dGame/dBehaviors/NpcCombatSkillBehavior.cpp index 69a6ea9dc..433594a2b 100644 --- a/dGame/dBehaviors/NpcCombatSkillBehavior.cpp +++ b/dGame/dBehaviors/NpcCombatSkillBehavior.cpp @@ -5,20 +5,40 @@ void NpcCombatSkillBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bit_stream, BehaviorBranchContext branch) { context->skillTime = this->m_npcSkillTime; + const auto* const targetEntity = Game::entityManager->GetEntity(branch.target); + const auto* const sourceEntity = Game::entityManager->GetEntity(context->caster); - for (auto* behavior : this->m_behaviors) { - behavior->Calculate(context, bit_stream, branch); + 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) { + 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() { 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(); - for (const auto& parameter : parameters) { - if (parameter.first.rfind("behavior", 0) == 0) { - auto* action = GetAction(parameter.second); + for (const auto& [parameter, value] : parameters) { + if (parameter.rfind("behavior", 0) == 0) { + auto* action = GetAction(value); this->m_behaviors.push_back(action); } diff --git a/dGame/dBehaviors/NpcCombatSkillBehavior.h b/dGame/dBehaviors/NpcCombatSkillBehavior.h index 07826f487..a1c7c0e9e 100644 --- a/dGame/dBehaviors/NpcCombatSkillBehavior.h +++ b/dGame/dBehaviors/NpcCombatSkillBehavior.h @@ -8,6 +8,9 @@ class NpcCombatSkillBehavior final : public Behavior float m_npcSkillTime; + float m_maxRange{}; + float m_minRange{}; + /* * Inherited */ diff --git a/dGame/dBehaviors/VerifyBehavior.cpp b/dGame/dBehaviors/VerifyBehavior.cpp index c7ede52f0..ec4b27a87 100644 --- a/dGame/dBehaviors/VerifyBehavior.cpp +++ b/dGame/dBehaviors/VerifyBehavior.cpp @@ -25,7 +25,7 @@ void VerifyBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS const auto distance = Vector3::DistanceSquared(self->GetPosition(), entity->GetPosition()); - if (distance > this->m_range * this->m_range) { + if (distance > this->m_range) { success = false; } } else if (this->m_blockCheck) { @@ -57,4 +57,5 @@ void VerifyBehavior::Load() { this->m_action = GetAction("action"); this->m_range = GetFloat("range"); + this->m_range = this->m_range * this->m_range * 0.9f; // Range checks are slightly smaller than the actual range to account for client/server discrepancies } diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index b97211b8e..9c3bc4be9 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -13,6 +13,8 @@ #include "CDClientDatabase.h" #include "CDClientManager.h" +#include "CDObjectSkillsTable.h" +#include "CDSkillBehaviorTable.h" #include "DestroyableComponent.h" #include @@ -43,7 +45,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo //Grab the aggro information from BaseCombatAI: auto componentQuery = CDClientDatabase::CreatePreppedStmt( - "SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;"); + "SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius, minRoundLength, maxRoundLength, combatRoundLength FROM BaseCombatAIComponent WHERE id = ?;"); componentQuery.bind(1, static_cast(componentID)); auto componentResult = componentQuery.execQuery(); @@ -63,44 +65,37 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo if (!componentResult.fieldIsNull("hardTetherRadius")) m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius"); - } - componentResult.finalize(); + m_MinRoundLength = componentResult.getFloatField("minRoundLength"); + m_MaxRoundLength = componentResult.getFloatField("maxRoundLength"); + m_CombatRoundLength = componentResult.getFloatField("combatRoundLength"); + } // 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. - if (m_Parent) { - auto aggroRadius = m_Parent->GetVar(u"aggroRadius"); - m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius; - auto tetherRadius = m_Parent->GetVar(u"tetherRadius"); - m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius; - } + m_AggroRadius = m_Parent->HasVar(u"aggroRadius") ? m_Parent->GetVar(u"aggroRadius") : m_AggroRadius; + m_HardTetherRadius = m_Parent->HasVar(u"tetherRadius") ? m_Parent->GetVar(u"tetherRadius") : m_HardTetherRadius; /* * Find skills */ - auto skillQuery = CDClientDatabase::CreatePreppedStmt( - "SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);"); - skillQuery.bind(1, static_cast(parent->GetLOT())); - - auto result = skillQuery.execQuery(); + for (const auto objectSkill : CDClientManager::GetTable()->Get(parent->GetLOT())) { + const auto skillBehavior = CDClientManager::GetTable()->GetSkillByID(objectSkill.skillID); + if (skillBehavior.skillID == objectSkill.skillID) { + const auto skillId = skillBehavior.skillID; - while (!result.eof()) { - const auto skillId = static_cast(result.getIntField("skillID")); + const auto abilityCooldown = skillBehavior.cooldown; - const auto abilityCooldown = static_cast(result.getFloatField("cooldown")); + const auto behaviorId = skillBehavior.behaviorID; - const auto behaviorId = static_cast(result.getIntField("behaviorID")); + const auto combatWeight = objectSkill.AICombatWeight; - auto* behavior = Behavior::CreateBehavior(behaviorId); + auto* behavior = Behavior::CreateBehavior(behaviorId); - std::stringstream behaviorQuery; + AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight }; - AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior }; - - m_SkillEntries.push_back(entry); - - result.nextRow(); + m_SkillEntries.push_back(entry); + } } Stun(1.0f); @@ -166,15 +161,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) { // Check if we should stop the tether effect if (m_TetherEffectActive) { m_TetherTime -= deltaTime; - if (m_Target != LWOOBJID_EMPTY || (NiPoint3::DistanceSquared( - m_StartPosition, - m_Parent->GetPosition()) < 20 * 20 && m_TetherTime <= 0) - ) { - GameMessages::SendStopFXEffect(m_Parent, true, "tether"); - m_TetherEffectActive = false; - } m_ForcedTetherTime -= deltaTime; if (m_ForcedTetherTime >= 0) return; + GameMessages::SendStopFXEffect(m_Parent, true, "tether"); + m_TetherEffectActive = false; } for (auto entry = m_RemovedThreatList.begin(); entry != m_RemovedThreatList.end();) { @@ -210,8 +200,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) { } if (stunnedThisFrame) { - m_MovementAI->Stop(); + if (!m_MovementAI->IsPaused()) m_MovementAI->Pause(); + // in this case we just become unstunned so check if we paused and resume if we did + if (!m_Stunned && m_MovementAI->IsPaused()) m_MovementAI->Resume(); return; } @@ -246,10 +238,12 @@ void BaseCombatAIComponent::Update(const float deltaTime) { void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { bool hasSkillToCast = false; + int32_t maxSkillWeights = 0; for (auto& entry : m_SkillEntries) { if (entry.cooldown > 0.0f) { entry.cooldown -= deltaTime; } else { + maxSkillWeights += entry.combatWeight; hasSkillToCast = true; } } @@ -317,12 +311,17 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { SetAiState(AiState::aggro); } else { SetAiState(AiState::idle); + // Don't clobber the pursuit speed set by TetherLogic while the tether is still active, + // otherwise the entity slows to wander speed and takes far longer than m_TetherTime to + // return to its spawn point. + if (m_MovementAI && !m_TetherEffectActive) m_MovementAI->SetMaxSpeed(1.0f); } if (!hasSkillToCast) return; if (m_Target == LWOOBJID_EMPTY) { SetAiState(AiState::idle); + if (m_MovementAI && !m_TetherEffectActive) m_MovementAI->SetMaxSpeed(1.0f); return; } @@ -333,14 +332,23 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { LookAt(target->GetPosition()); } - for (auto i = 0; i < m_SkillEntries.size(); ++i) { - auto entry = m_SkillEntries.at(i); + // Roll to find which skill we'll try to cast + auto randomizedWeight = GeneralUtils::GenerateRandomNumber(0, maxSkillWeights); + + for (auto& entry : m_SkillEntries) { + // Skill isn't cooled off yet + if (entry.cooldown > 0.0f) { + continue; + } + + randomizedWeight -= entry.combatWeight; - if (entry.cooldown > 0) { + // 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, LWOOBJID_EMPTY); + const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, GetTarget()); if (result.success) { if (m_MovementAI != nullptr) { @@ -355,8 +363,6 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { entry.cooldown = entry.abilityCooldown + m_SkillTime; - m_SkillEntries[i] = entry; - break; } } @@ -513,6 +519,10 @@ void BaseCombatAIComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsI void BaseCombatAIComponent::SetAiState(AiState newState) { if (newState == this->m_State) return; + GameMessages::NotifyCombatAIStateChange stateMsg; + stateMsg.prevState = this->m_State; + stateMsg.newState = newState; + m_Parent->HandleMsg(stateMsg); this->m_State = newState; m_DirtyStateOrTarget = true; Game::entityManager->SerializeEntity(m_Parent); @@ -618,6 +628,11 @@ void BaseCombatAIComponent::Wander() { return; } + // If we have a path to follow we should almost certainly do that instead of wandering. + if (m_MovementAI->HasPath()) { + return; + } + m_MovementAI->SetHaltDistance(0); const auto& info = m_MovementAI->GetInfo(); @@ -746,8 +761,8 @@ void BaseCombatAIComponent::SetTetherSpeed(float value) { m_TetherSpeed = value; } -void BaseCombatAIComponent::Stun(const float time) { - if (m_StunImmune || m_StunTime > time) { +void BaseCombatAIComponent::Stun(const float time, const bool force) { + if (!force && (m_StunImmune || m_StunTime > time)) { return; } @@ -793,7 +808,9 @@ void BaseCombatAIComponent::Wake() { void BaseCombatAIComponent::TetherLogic() { auto* destroyableComponent = m_Parent->GetComponent(); - if (destroyableComponent != nullptr && destroyableComponent->HasFaction(4)) { + const bool applyTetherEffect = destroyableComponent != nullptr && destroyableComponent->HasFaction(4); + + if (applyTetherEffect) { auto serilizationRequired = false; if (destroyableComponent->GetHealth() != destroyableComponent->GetMaxHealth()) { @@ -815,16 +832,27 @@ void BaseCombatAIComponent::TetherLogic() { GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), 6270, u"tether", "tether"); m_TetherEffectActive = true; - - m_TetherTime = 3.0f; } // Speed towards start position if (m_MovementAI != nullptr) { m_MovementAI->SetHaltDistance(0); m_MovementAI->SetMaxSpeed(m_PursuitSpeed); + m_MovementAI->SetCurrentSpeed(m_PursuitSpeed); m_MovementAI->SetDestination(m_StartPosition); } + + if (applyTetherEffect) { + // Use the actual navmesh path length (populated by SetDestination above) instead of the + // straight-line distance, otherwise the tether timer expires before the entity can walk + // around obstacles back to its spawn point. + const auto distance = m_MovementAI != nullptr + ? m_MovementAI->GetRemainingPathDistance() + : NiPoint3::Distance(m_Parent->GetPosition(), m_StartPosition); + const auto speed = m_PursuitSpeed * MovementAIComponent::GetBaseSpeed(m_Parent->GetLOT()); + m_TetherTime = speed > 0.0f ? distance / speed : 0.0f; + m_ForcedTetherTime = m_TetherTime; + } } void BaseCombatAIComponent::ForceTether() { @@ -847,7 +875,7 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport auto& cmptType = reportInfo.info->PushDebug("Base Combat AI"); cmptType.PushDebug("Component ID") = GetComponentID(); auto& targetInfo = cmptType.PushDebug("Current Target Info"); - targetInfo.PushDebug("Current Target ID") = std::to_string(m_Target); + targetInfo.PushDebug("Current Target ID", "LWOOBJID") = std::to_string(m_Target); // if (m_Target != LWOOBJID_EMPTY) { // LWOGameMessages::ObjGetName nameMsg(m_CurrentTarget); // SEND_GAMEOBJ_MSG(nameMsg); @@ -862,12 +890,12 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport // roundInfo.PushDebug("Combat Start Delay") = m_CombatStartDelay; std::string curState; switch (m_State) { - case idle: curState = "Idling"; break; - case aggro: curState = "Aggroed"; break; - case tether: curState = "Returning to Tether"; break; - case spawn: curState = "Spawn"; break; - case dead: curState = "Dead"; break; - default: curState = "Unknown or Undefined"; break; + case idle: curState = "Idling"; break; + case aggro: curState = "Aggroed"; break; + case tether: curState = "Returning to Tether"; break; + case spawn: curState = "Spawn"; break; + case dead: curState = "Dead"; break; + default: curState = "Unknown or Undefined"; break; } cmptType.PushDebug("Current Combat State") = curState; @@ -888,10 +916,7 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport //} //cmptType.PushDebug("Current Combat Role") = curState; - auto& tetherPoint = cmptType.PushDebug("Tether Point"); - tetherPoint.PushDebug("X") = m_StartPosition.x; - tetherPoint.PushDebug("Y") = m_StartPosition.y; - tetherPoint.PushDebug("Z") = m_StartPosition.z; + cmptType.PushDebug("Tether Point").PushDebug(m_StartPosition); cmptType.PushDebug("Hard Tether Radius") = m_HardTetherRadius; cmptType.PushDebug("Soft Tether Radius") = m_SoftTetherRadius; cmptType.PushDebug("Aggro Radius") = m_AggroRadius; @@ -905,8 +930,16 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport } auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats"); - for (const auto& [id, threat] : m_ThreatEntries) { + for (const auto& [id, threat] : m_RemovedThreatList) { ignoredThreats.PushDebug(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("Cooldown") = skill.cooldown; + skillDebug.PushDebug("Ability Cooldown") = skill.abilityCooldown; + skillDebug.PushDebug("AI Combat Weight") = skill.combatWeight; + } + return true; } diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 95eb75ced..95a63c161 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -33,13 +33,15 @@ enum class AiState : uint32_t { */ struct AiSkillEntry { - uint32_t skillId; + uint32_t skillId{}; - float cooldown; + float cooldown{}; - float abilityCooldown; + float abilityCooldown{}; - Behavior* behavior; + Behavior* behavior{}; + + int32_t combatWeight{}; }; /** @@ -181,8 +183,9 @@ class BaseCombatAIComponent final : public Component { /** * 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 force whether or not to force the stun and ignore checks */ - void Stun(float time); + void Stun(float time, const bool force = false); /** * Gets the radius that will cause this entity to get aggro'd, causing a target chase @@ -236,6 +239,12 @@ class BaseCombatAIComponent final : public Component { bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + void SetStartingPosition(const NiPoint3& pos) { m_StartPosition = pos; } + + bool GetOutOfCombat() const { return m_OutOfCombat; } + + bool GetIsTethering() const { return m_TetherEffectActive; } + private: /** * Returns the current target or the target that currently is the largest threat to this entity @@ -394,9 +403,17 @@ class BaseCombatAIComponent final : public Component { */ 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 float m_ForcedTetherTime = 0.0f; + float m_CombatRoundLength = 0.0f; + // The amount of time a removed threat will be ignored for. std::map m_RemovedThreatList; diff --git a/dGame/dComponents/ControllablePhysicsComponent.cpp b/dGame/dComponents/ControllablePhysicsComponent.cpp index 2b26368f7..86be8622b 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/ControllablePhysicsComponent.cpp @@ -361,17 +361,11 @@ void ControllablePhysicsComponent::SetStunImmunity( bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { PhysicsComponent::OnGetObjectReportInfo(reportInfo); - auto& info = reportInfo.subCategory->PushDebug("Controllable Info"); + auto& info = reportInfo.subCategory->PushDebug("Controllable Physics"); - auto& vel = info.PushDebug("Velocity"); - vel.PushDebug("x") = m_Velocity.x; - vel.PushDebug("y") = m_Velocity.y; - vel.PushDebug("z") = m_Velocity.z; + info.PushDebug("Velocity").PushDebug(m_Velocity); - auto& angularVelocity = info.PushDebug("Angular Velocity"); - angularVelocity.PushDebug("x") = m_AngularVelocity.x; - angularVelocity.PushDebug("y") = m_AngularVelocity.y; - angularVelocity.PushDebug("z") = m_AngularVelocity.z; + info.PushDebug("Angular Velocity").PushDebug(m_AngularVelocity); info.PushDebug("Is On Ground") = m_IsOnGround; info.PushDebug("Is On Rail") = m_IsOnRail; @@ -403,12 +397,13 @@ bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObject info.PushDebug("Is In Bubble") = m_IsInBubble; info.PushDebug("Bubble Type") = StringifiedEnum::ToString(m_BubbleType).data(); info.PushDebug("Special Anims") = m_SpecialAnims; - info.PushDebug("Immune To Stun Attack Count") = m_ImmuneToStunAttackCount; - info.PushDebug("Immune To Stun Equip Count") = m_ImmuneToStunEquipCount; - info.PushDebug("Immune To Stun Interact Count") = m_ImmuneToStunInteractCount; - info.PushDebug("Immune To Stun Jump Count") = m_ImmuneToStunJumpCount; - info.PushDebug("Immune To Stun Move Count") = m_ImmuneToStunMoveCount; - info.PushDebug("Immune To Stun Turn Count") = m_ImmuneToStunTurnCount; - info.PushDebug("Immune To Stun UseItem Count") = m_ImmuneToStunUseItemCount; + auto& immunity = info.PushDebug("Immunity Effects"); + immunity.PushDebug("Immune to Stun Move") = m_ImmuneToStunMoveCount != 0; + immunity.PushDebug("Immune to Stun Turn") = m_ImmuneToStunTurnCount != 0; + immunity.PushDebug("Immune to Stun Attack") = m_ImmuneToStunAttackCount != 0; + immunity.PushDebug("Immune to Stun Use Item") = m_ImmuneToStunUseItemCount != 0; + immunity.PushDebug("Immune to Stun Equip") = m_ImmuneToStunEquipCount != 0; + immunity.PushDebug("Immune to Stun Interact") = m_ImmuneToStunInteractCount != 0; + immunity.PushDebug("Immune to Stun Jump") = m_ImmuneToStunJumpCount != 0; return true; } diff --git a/dGame/dComponents/ControllablePhysicsComponent.h b/dGame/dComponents/ControllablePhysicsComponent.h index 76de5b516..d3230ca2c 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.h +++ b/dGame/dComponents/ControllablePhysicsComponent.h @@ -328,7 +328,7 @@ class ControllablePhysicsComponent : public PhysicsComponent { /** * The effect that plays while using the jetpack */ - int32_t m_JetpackEffectID; + int32_t m_JetpackEffectID{}; /** * The current speed multiplier, allowing an entity to run faster diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 905913671..877fe4fa8 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -1146,7 +1146,7 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportIn destroyableInfo.PushDebug("Explode Factor") = m_ExplodeFactor; destroyableInfo.PushDebug("Has Threats") = m_HasThreats; - destroyableInfo.PushDebug("Killer ID") = std::to_string(m_KillerID); + destroyableInfo.PushDebug("Killer ID", "LWOOBJID") = std::to_string(m_KillerID); // "Scripts"; idk what to do about scripts yet auto& immuneCounts = destroyableInfo.PushDebug("Immune Counts"); diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index c27cd31a7..e55031d4f 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -108,7 +108,7 @@ bool GhostComponent::OnGetGMInvis(GameMessages::GetGMInvis& gmInvisMsg) { bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) { auto& cmptType = reportMsg.info->PushDebug("Ghost"); cmptType.PushDebug("Component ID") = GetComponentID(); - cmptType.PushDebug("Is GM Invis") = false; + cmptType.PushDebug("Is GM Invis") = m_IsGMInvisible; return true; } diff --git a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp index 52819657d..299c2c5a5 100644 --- a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp +++ b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp @@ -110,15 +110,9 @@ bool HavokVehiclePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObject auto& info = reportInfo.subCategory->PushDebug("Havok Vehicle Physics Info"); - auto& velocity = info.PushDebug("Velocity"); - velocity.PushDebug("x") = m_Velocity.x; - velocity.PushDebug("y") = m_Velocity.y; - velocity.PushDebug("z") = m_Velocity.z; - - auto& angularVelocity = info.PushDebug("Angular Velocity"); - angularVelocity.PushDebug("x") = m_AngularVelocity.x; - angularVelocity.PushDebug("y") = m_AngularVelocity.y; - angularVelocity.PushDebug("z") = m_AngularVelocity.z; + auto& velocity = info.PushDebug("Velocity").PushDebug(m_Velocity); + + auto& angularVelocity = info.PushDebug("Angular Velocity").PushDebug(m_AngularVelocity); info.PushDebug("Is On Ground") = m_IsOnGround; info.PushDebug("Is On Rail") = m_IsOnRail; diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 0d410e82d..c97bf41a1 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -751,10 +751,6 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b for (const auto& pair : m_Equipped) { const auto item = pair.second; - if (bIsInitialUpdate) { - AddItemSkills(item.lot); - } - outBitStream.Write(item.id); outBitStream.Write(item.lot); @@ -1012,9 +1008,6 @@ void InventoryComponent::UnequipScripts(Item* unequippedItem) { } void InventoryComponent::HandlePossession(Item* item) { - auto* characterComponent = m_Parent->GetComponent(); - if (!characterComponent) return; - auto* possessorComponent = m_Parent->GetComponent(); if (!possessorComponent) return; @@ -1030,52 +1023,31 @@ void InventoryComponent::HandlePossession(Item* item) { return; } - GameMessages::SendSetStunned(m_Parent->GetObjectID(), eStateChangeType::PUSH, m_Parent->GetSystemAddress(), LWOOBJID_EMPTY, true, false, true, false, false, false, false, true, true, true, true, true, true, true, true, true); - - // Set the mount Item ID so that we know what were handling + // Set the mount item ID so that we know what we're handling possessorComponent->SetMountItemID(item->GetId()); GameMessages::SendSetMountInventoryID(m_Parent, item->GetId(), UNASSIGNED_SYSTEM_ADDRESS); - // Create entity to mount - auto startRotation = m_Parent->GetRotation(); - + // Create the mount entity EntityInfo info{}; info.lot = item->GetLot(); info.pos = m_Parent->GetPosition(); - info.rot = startRotation; + info.rot = m_Parent->GetRotation(); info.spawnerID = m_Parent->GetObjectID(); auto* mount = Game::entityManager->CreateEntity(info, nullptr, m_Parent); - // Check to see if the mount is a vehicle, if so, flip it - auto* vehicleComponent = mount->GetComponent(); - if (vehicleComponent) characterComponent->SetIsRacing(true); - - // Setup the destroyable stats - auto* destroyableComponent = mount->GetComponent(); - if (destroyableComponent) { - destroyableComponent->SetIsImmune(true); - } - - // Mount it auto* possessableComponent = mount->GetComponent(); if (possessableComponent) { possessableComponent->SetIsItemSpawned(true); possessableComponent->SetPossessor(m_Parent->GetObjectID()); - // Possess it - possessorComponent->SetPossessable(mount->GetObjectID()); - possessorComponent->SetPossessableType(possessableComponent->GetPossessionType()); } - GameMessages::SendSetJetPackMode(m_Parent, false); + auto* destroyableComponent = mount->GetComponent(); + if (destroyableComponent) destroyableComponent->SetIsImmune(true); - // Make it go to the client Game::entityManager->ConstructEntity(mount); - // Update the possessor - Game::entityManager->SerializeEntity(m_Parent); + possessorComponent->Mount(mount); - // have to unlock the input so it vehicle can be driven - if (vehicleComponent) GameMessages::SendVehicleUnlockInput(mount->GetObjectID(), false, m_Parent->GetSystemAddress()); GameMessages::SendMarkInventoryItemAsActive(m_Parent->GetObjectID(), true, eUnequippableActiveType::MOUNT, item->GetId(), m_Parent->GetSystemAddress()); } @@ -1203,14 +1175,12 @@ LOT InventoryComponent::GetConsumable() const { void InventoryComponent::AddItemSkills(const LOT lot) { const auto info = Inventory::FindItemComponent(lot); - const auto slot = FindBehaviorSlot(static_cast(info.itemType)); + const auto slot = FindBehaviorSlot(info.equipLocation); if (slot == BehaviorSlot::Invalid) { return; } - const auto index = m_Skills.find(slot); - const auto skill = FindSkill(lot); SetSkill(slot, skill); @@ -1238,7 +1208,7 @@ void InventoryComponent::FixInvisibleItems() { void InventoryComponent::RemoveItemSkills(const LOT lot) { const auto info = Inventory::FindItemComponent(lot); - const auto slot = FindBehaviorSlot(static_cast(info.itemType)); + const auto slot = FindBehaviorSlot(info.equipLocation); if (slot == BehaviorSlot::Invalid) { return; @@ -1250,15 +1220,31 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) { return; } - const auto old = index->second; + const auto skillId = FindSkill(lot); - GameMessages::SendRemoveSkill(m_Parent, old); + // Only act on this slot if it still holds the skill from this item. + // Another item may have overwritten the slot since this one was equipped. + if (index->second != skillId) { + return; + } m_Skills.erase(slot); + // Find another slot that still holds this skillID (if any). + const auto surviving = std::ranges::find_if(m_Skills, [skillId](const auto& pair) { + return pair.second == skillId; + }); + + // The client stores one acquiredSkillsInfo entry per skillID, tagged with the slotID + // it was originally added with. Always send RemoveSkill to clear that entry, then + // re-add with the surviving slot so the client shows it in the correct place. + GameMessages::SendRemoveSkill(m_Parent, skillId); + if (surviving != m_Skills.end()) { + GameMessages::SendAddSkill(m_Parent, skillId, surviving->first); + } + if (slot == BehaviorSlot::Primary) { m_Skills.insert_or_assign(BehaviorSlot::Primary, 1); - GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary); } } @@ -1350,23 +1336,17 @@ void InventoryComponent::RemoveDatabasePet(LWOOBJID id) { m_Pets.erase(id); } -BehaviorSlot InventoryComponent::FindBehaviorSlot(const eItemType type) { - switch (type) { - case eItemType::HAT: - return BehaviorSlot::Head; - case eItemType::NECK: - return BehaviorSlot::Neck; - case eItemType::LEFT_HAND: - return BehaviorSlot::Offhand; - case eItemType::RIGHT_HAND: - return BehaviorSlot::Primary; - case eItemType::CONSUMABLE: - return BehaviorSlot::Consumable; - default: - return BehaviorSlot::Invalid; - } +BehaviorSlot InventoryComponent::FindBehaviorSlot(const std::string& equipLocation) { + // Skill slot is determined by equipLocation, not itemType. + // Mapping confirmed against live captures and client data (issue #1339). + if (equipLocation == "special_r") return BehaviorSlot::Primary; + if (equipLocation == "hair") return BehaviorSlot::Head; + if (equipLocation == "special_l") return BehaviorSlot::Offhand; + if (equipLocation == "clavicle") return BehaviorSlot::Neck; + return BehaviorSlot::Invalid; } + bool InventoryComponent::IsTransferInventory(eInventoryType type, bool includeVault) { return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB; } @@ -1707,10 +1687,28 @@ bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) { const auto index = m_Skills.find(slot); if (index != m_Skills.end()) { const auto old = index->second; - GameMessages::SendRemoveSkill(m_Parent, old); + // Only remove the old skill from the client if no other slot still holds it. + // The client's acquiredSkillsInfo is keyed by skillID (one entry per skill), + // so RemoveSkill clears it globally — sending it while another slot still uses + // the same skillID would break that slot on the client. + const auto usedElsewhere = std::ranges::any_of(m_Skills, [&](const auto& pair) { + return pair.first != slot && pair.second == old; + }); + if (!usedElsewhere) { + GameMessages::SendRemoveSkill(m_Parent, old); + } + } + + // Only send AddSkill if the client doesn't already know about this skillID. + // The client early-exits on duplicate AddSkill (same skillID already in + // acquiredSkillsInfo) without updating the slot — so only send when it's new. + const auto alreadyKnown = std::ranges::any_of(m_Skills, [&](const auto& pair) { + return pair.first != slot && pair.second == skillId; + }); + if (!alreadyKnown) { + GameMessages::SendAddSkill(m_Parent, skillId, slot); } - GameMessages::SendAddSkill(m_Parent, skillId, slot); m_Skills.insert_or_assign(slot, skillId); return true; } @@ -1890,9 +1888,9 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo std::stringstream ss; ss << "%[Objects_" << item->GetLot() << "_name] Slot " << item->GetSlot(); auto& slot = curInv.PushDebug(ss.str()); - slot.PushDebug("Object ID") = std::to_string(item->GetId()); - slot.PushDebug("LOT") = item->GetLot(); - if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug("Subkey") = std::to_string(item->GetSubKey()); + slot.PushDebug("Object ID", "LWOOBJID") = std::to_string(item->GetId()); + slot.PushDebug("LOT", "LOT") = item->GetLot(); + if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug("Subkey", "LWOOBJID") = std::to_string(item->GetSubKey()); slot.PushDebug("Count") = item->GetCount(); slot.PushDebug("Slot") = item->GetSlot(); slot.PushDebug("Bind on pickup") = item->GetInfo().isBOP; @@ -1911,7 +1909,8 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo ss << "%[Objects_" << info.lot << "_name]"; auto& equipSlot = equipped.PushDebug(ss.str()); equipSlot.PushDebug("Location") = location; - equipSlot.PushDebug("Object ID") = std::to_string(info.id); + equipSlot.PushDebug("Object ID", "LWOOBJID") = std::to_string(info.id); + equipSlot.PushDebug("LOT", "LOT") = info.lot; equipSlot.PushDebug("Slot") = info.slot; equipSlot.PushDebug("Count") = info.count; auto& extra = equipSlot.PushDebug("Extra Info"); diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index e05fb870c..dde3cf738 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -368,11 +368,10 @@ class InventoryComponent final : public Component { void RemoveDatabasePet(LWOOBJID id); /** - * Returns the current behavior slot active for the passed item type - * @param type the item type to find the behavior slot for - * @return the current behavior slot active for the passed item type + * Returns the behavior slot for the given equipLocation string. + * This is the authoritative mapping used for skill slot assignment. */ - static BehaviorSlot FindBehaviorSlot(eItemType type); + static BehaviorSlot FindBehaviorSlot(const std::string& equipLocation); /** * Checks if the inventory type is a temp inventory @@ -404,6 +403,8 @@ class InventoryComponent final : public Component { std::map GetSkills() { return m_Skills; }; + void ClearSkills() { m_Skills.clear(); }; + bool SetSkill(int slot, uint32_t skillId); bool SetSkill(BehaviorSlot slot, uint32_t skillId); diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index 08c4ce8d6..7ba24a4a8 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -652,7 +652,7 @@ void PushMissions(const std::map& missions, AMFArrayValue& V } bool MissionComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { - auto& missionInfo = reportInfo.info->PushDebug("Mission (Laggy)"); + auto& missionInfo = reportInfo.info->PushDebug("Mission (Laggy)", "", 1); missionInfo.PushDebug("Component ID") = GetComponentID(); // Sort the missions so they are easier to parse and present to the end user std::map achievements; diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 48585237b..248cbbac3 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -355,8 +355,8 @@ bool ModelComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& re cmptInfo.PushDebug("Name") = "Objects_" + std::to_string(m_Parent->GetLOT()) + "_name"; cmptInfo.PushDebug("Has Unique Name") = false; - cmptInfo.PushDebug("UGID (from item)") = std::to_string(m_userModelID); - cmptInfo.PushDebug("UGID") = std::to_string(m_userModelID); + cmptInfo.PushDebug("UGID (from item)", "LWOOBJID") = std::to_string(m_userModelID); + cmptInfo.PushDebug("UGID", "LWOOBJID") = std::to_string(m_userModelID); cmptInfo.PushDebug("Description") = ""; cmptInfo.PushDebug("Behavior Count") = m_Behaviors.size(); diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index 3519712a1..f5ef9f367 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -19,6 +19,12 @@ #include "Amf3.h" #include "dNavMesh.h" +#include "eWaypointCommandType.h" +#include "StringifiedEnum.h" +#include "SkillComponent.h" +#include "GeneralUtils.h" +#include "RenderComponent.h" +#include "InventoryComponent.h" namespace { /** @@ -31,8 +37,6 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component m_Info = info; m_AtFinalWaypoint = true; - m_BaseCombatAI = nullptr; - m_BaseCombatAI = m_Parent->GetComponent(); //Try and fix the insane values: @@ -60,7 +64,7 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo); - if (!m_Parent->GetComponent()) SetPath(m_Parent->GetVarAsString(u"attached_path")); + SetPath(m_Parent->GetVarAsString(u"attached_path")); } void MovementAIComponent::SetPath(const std::string pathName) { @@ -125,7 +129,15 @@ void MovementAIComponent::Update(const float deltaTime) { m_TimeTravelled += deltaTime; - SetPosition(ApproximateLocation()); + const auto approxPos = ApproximateLocation(); + SetPosition(approxPos); + // Set the AIs new home based on where our current waypoint is IF wehave a path, we're idle, not out of combat (grace period if you left the radius), + // and we're not tethering to home, that way we can return to this + // when resuming the pathing after losing aggro while moving the aggro hitbox with us + const bool idleNotResetting = m_Path && m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle && !m_BaseCombatAI->GetOutOfCombat() && !m_BaseCombatAI->GetIsTethering(); + if (idleNotResetting) { + m_BaseCombatAI->SetStartingPosition(approxPos); + } if (m_TimeTravelled < m_TimeToTravel) return; m_TimeTravelled = 0.0f; @@ -160,32 +172,45 @@ void MovementAIComponent::Update(const float deltaTime) { SetRotation(QuatUtils::LookAt(source, m_NextWaypoint)); } } else { - // 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; - if (m_CurrentPath.empty()) { - if (m_Path) { - if (m_Path->pathBehavior == PathBehavior::Loop) { - SetPath(m_Path->pathWaypoints); - } else if (m_Path->pathBehavior == PathBehavior::Bounce) { - m_IsBounced = !m_IsBounced; - std::vector waypoints = m_Path->pathWaypoints; - if (m_IsBounced) std::ranges::reverse(waypoints); - SetPath(waypoints); - } else if (m_Path->pathBehavior == PathBehavior::Once) { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + // 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 || (idleNotResetting)) { + // 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; + RunWaypointCommands(waypointNum); + if (m_CurrentPath.empty()) { + if (m_Path) { + if (m_Path->pathBehavior == PathBehavior::Loop) { + SetPath(m_Path->pathWaypoints); + } else if (m_Path->pathBehavior == PathBehavior::Bounce) { + m_IsBounced = !m_IsBounced; + std::vector waypoints = m_Path->pathWaypoints; + if (m_IsBounced) std::ranges::reverse(waypoints); + SetPath(waypoints); + } 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. + Stop(); + return; + } + } else { Stop(); + if (m_FollowedTarget != LWOOBJID_EMPTY) { + GameMessages::GetPosition getPos; + if (!getPos.Send(m_FollowedTarget)) { + LOG("Target %llu does not exist anymore to follow", m_FollowedTarget); + m_FollowedTarget = LWOOBJID_EMPTY; + } else { + SetDestination(getPos.pos); + } + } return; } } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); - Stop(); - return; + SetDestination(m_CurrentPath.top().position); + + m_CurrentPath.pop(); } } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); - SetDestination(m_CurrentPath.top().position); - - m_CurrentPath.pop(); + Stop(); } } @@ -212,8 +237,7 @@ NiPoint3 MovementAIComponent::GetCurrentWaypoint() const { NiPoint3 MovementAIComponent::ApproximateLocation() const { auto source = m_SourcePosition; - - if (AtFinalWaypoint()) return source; + if (AtFinalWaypoint()) return m_Parent->GetPosition(); auto destination = m_NextWaypoint; @@ -323,6 +347,18 @@ float MovementAIComponent::GetBaseSpeed(LOT lot) { return speed; } +float MovementAIComponent::GetRemainingPathDistance() const { + if (m_InterpolatedWaypoints.empty() || m_PathIndex >= m_InterpolatedWaypoints.size()) { + return 0.0f; + } + + float total = NiPoint3::Distance(m_Parent->GetPosition(), m_InterpolatedWaypoints[m_PathIndex]); + for (size_t i = m_PathIndex; i + 1 < m_InterpolatedWaypoints.size(); ++i) { + total += NiPoint3::Distance(m_InterpolatedWaypoints[i], m_InterpolatedWaypoints[i + 1]); + } + return total; +} + void MovementAIComponent::SetPosition(const NiPoint3& value) { m_Parent->SetPosition(value); } @@ -423,7 +459,69 @@ NiPoint3 MovementAIComponent::GetDestination() const { void MovementAIComponent::SetMaxSpeed(const float value) { if (value == m_MaxSpeed) return; m_MaxSpeed = value; - m_Acceleration = value / 5; + m_Acceleration = value / 5.0f; +} + +void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) { + m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + + if (!m_Path || waypointNum >= m_Path->pathWaypoints.size()) return; + const auto& commands = m_Path->pathWaypoints[waypointNum].commands; + for (const auto& [command, data] : commands) { + LOG_DEBUG("%s %s %s", StringifiedEnum::ToString(command).data(), m_Path->pathName.c_str(), data.c_str()); + const auto dataSplit = GeneralUtils::SplitString(data, ','); + switch (command) { + case eWaypointCommandType::INVALID: break; + case eWaypointCommandType::BOUNCE: break; + case eWaypointCommandType::STOP: Pause(); break; + case eWaypointCommandType::GROUP_EMOTE: break; + case eWaypointCommandType::SET_VARIABLE: break; // Empty in the client + case eWaypointCommandType::CAST_SKILL: { + const auto skill = GeneralUtils::TryParse(data); + if (skill) { + auto* const skillComponent = m_Parent->GetComponent(); + if (skillComponent) skillComponent->CastSkill(skill.value()); + } + break; + } + case eWaypointCommandType::EQUIP_INVENTORY: { + auto* const inventoryComponent = m_Parent->GetComponent(); + if (inventoryComponent) { + // items should always exist + auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0); + if (item) inventoryComponent->EquipItem(item); + } + break; + } + case eWaypointCommandType::UNEQUIP_INVENTORY: { + auto* const inventoryComponent = m_Parent->GetComponent(); + if (inventoryComponent) { + // items should always exist + auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0); + if (item) inventoryComponent->UnEquipItem(item); + } + break; + } + case eWaypointCommandType::DELAY: { + // Pause(GeneralUtils::TryParse(data).value_or(0.0f)); + break; + } + case eWaypointCommandType::EMOTE: { + // m_Delay = RenderComponent::GetAnimationTime(m_Parent, data); + // const auto emoteID = GeneralUtils::TryParse(data); + // if (emoteID) GameMessages::SendPlayEmote(m_Parent->GetObjectID(), emoteID.value(), LWOOBJID_EMPTY, UNASSIGNED_SYSTEM_ADDRESS); + break; + } + case eWaypointCommandType::TELEPORT: break; + case eWaypointCommandType::PATH_SPEED: m_BaseSpeed = GetBaseSpeed(m_Parent->GetLOT()) * GeneralUtils::TryParse(data).value_or(1.0f); break; + case eWaypointCommandType::REMOVE_NPC: break; + case eWaypointCommandType::CHANGE_WAYPOINT: SetPath(dataSplit[0]); break; + case eWaypointCommandType::DELETE_SELF: break; + case eWaypointCommandType::KILL_SELF: m_Parent->Smash(); break; + case eWaypointCommandType::SPAWN_OBJECT: break; + case eWaypointCommandType::PLAY_SOUND: break; + } + } } bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { @@ -453,21 +551,16 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf movementInfo.PushDebug("Lock Rotation") = m_LockRotation; movementInfo.PushDebug("Paused") = m_Paused; movementInfo.PushDebug("Pulling To Point") = m_PullingToPoint; + movementInfo.PushDebug("At Final Waypoint") = m_AtFinalWaypoint; + + auto& pullPointInfo = movementInfo.PushDebug("Pull Point").PushDebug(m_PullPoint); - auto& pullPointInfo = movementInfo.PushDebug("Pull Point"); - pullPointInfo.PushDebug("X") = m_PullPoint.x; - pullPointInfo.PushDebug("Y") = m_PullPoint.y; - pullPointInfo.PushDebug("Z") = m_PullPoint.z; - // movementInfo.PushDebug("Delay") = m_Delay; auto& waypoints = movementInfo.PushDebug("Interpolated Waypoints"); int i = 0; for (const auto& point : m_InterpolatedWaypoints) { - auto& waypoint = waypoints.PushDebug("Waypoint " + std::to_string(++i)); - waypoint.PushDebug("X") = point.x; - waypoint.PushDebug("Y") = point.y; - waypoint.PushDebug("Z") = point.z; + waypoints.PushDebug("Waypoint " + std::to_string(++i)).PushDebug(point); } i = 0; @@ -475,13 +568,30 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf auto pathCopy = m_CurrentPath; // Copy to avoid modifying the original stack while (!pathCopy.empty()) { const auto& waypoint = pathCopy.top(); - auto& pathWaypoint = currentPath.PushDebug("Waypoint " + std::to_string(++i)); - pathWaypoint.PushDebug("X") = waypoint.position.x; - pathWaypoint.PushDebug("Y") = waypoint.position.y; - pathWaypoint.PushDebug("Z") = waypoint.position.z; + currentPath.PushDebug("Waypoint " + std::to_string(++i)).PushDebug(waypoint.position); pathCopy.pop(); } + movementInfo.PushDebug("Followed Target") = std::to_string(m_FollowedTarget); + return true; } + +void MovementAIComponent::FollowTarget(const LWOOBJID target) { + if (target == LWOOBJID_EMPTY) { + m_FollowedTarget = target; + return; + } + GameMessages::GetPosition getPos; + if (!getPos.Send(target)) { + LOG("Tried to follow target %llu but they don't exist", target); + m_FollowedTarget = LWOOBJID_EMPTY; + return; + } + + m_FollowedTarget = target; + SetMaxSpeed(1.0f); + m_CurrentSpeed = 1.0f; + SetDestination(getPos.pos); +} diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 2a0957164..f19007fc7 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -31,27 +31,27 @@ struct MovementAIInfo { /** * The radius that the entity can wander in */ - float wanderRadius; + float wanderRadius{}; /** * The speed at which the entity wanders */ - float wanderSpeed; + float wanderSpeed{}; /** * This is only used for the emotes */ - float wanderChance; + float wanderChance{}; /** * The min amount of delay before wandering */ - float wanderDelayMin; + float wanderDelayMin{}; /** * The max amount of delay before wandering */ - float wanderDelayMax; + float wanderDelayMax{}; }; /** @@ -209,11 +209,28 @@ class MovementAIComponent final : public Component { */ static float GetBaseSpeed(LOT lot); + /** + * Returns the total distance remaining along the current path, following the + * interpolated waypoints from the entity's current position to the final destination. + * @return the total remaining path distance in world units + */ + float GetRemainingPathDistance() const; + bool IsPaused() const { return m_Paused; } bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + + bool HasPath() const { return m_Path != nullptr; } + + void FollowTarget(const LWOOBJID target); private: + /** + * @brief + * Runs the commands on a waypoint if a path exists + */ + void RunWaypointCommands(uint32_t waypointNum); + /** * Sets the current position of the entity * @param value the position to set @@ -329,6 +346,8 @@ class MovementAIComponent final : public Component { // The number of waypoints that were on the path in the call to SetPath uint32_t m_CurrentPathWaypointCount{ 0 }; + + LWOOBJID m_FollowedTarget{ LWOOBJID_EMPTY }; }; #endif // MOVEMENTAICOMPONENT_H diff --git a/dGame/dComponents/PhantomPhysicsComponent.cpp b/dGame/dComponents/PhantomPhysicsComponent.cpp index a65df484a..9a90dbe37 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.cpp +++ b/dGame/dComponents/PhantomPhysicsComponent.cpp @@ -238,10 +238,7 @@ bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectRepor info.PushDebug("Effect Type") = static_cast(m_EffectType); info.PushDebug("Directional Multiplier") = m_DirectionalMultiplier; info.PushDebug("Is Directional") = m_IsDirectional; - auto& direction = info.PushDebug("Direction"); - direction.PushDebug("x") = m_Direction.x; - direction.PushDebug("y") = m_Direction.y; - direction.PushDebug("z") = m_Direction.z; + auto& direction = info.PushDebug("Direction").PushDebug(m_Direction); if (m_MinMax) { auto& minMaxInfo = info.PushDebug("Min Max Info"); @@ -252,15 +249,8 @@ bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectRepor if (m_IsRespawnVolume) { auto& respawnInfo = info.PushDebug("Respawn Info"); respawnInfo.PushDebug("Is Respawn Volume") = m_IsRespawnVolume; - auto& respawnPos = respawnInfo.PushDebug("Respawn Position"); - respawnPos.PushDebug("x") = m_RespawnPos.x; - respawnPos.PushDebug("y") = m_RespawnPos.y; - respawnPos.PushDebug("z") = m_RespawnPos.z; - auto& respawnRot = respawnInfo.PushDebug("Respawn Rotation"); - respawnRot.PushDebug("w") = m_RespawnRot.w; - respawnRot.PushDebug("x") = m_RespawnRot.x; - respawnRot.PushDebug("y") = m_RespawnRot.y; - respawnRot.PushDebug("z") = m_RespawnRot.z; + respawnInfo.PushDebug("Respawn Position").PushDebug(m_RespawnPos); + respawnInfo.PushDebug("Respawn Rotation").PushDebug(m_RespawnRot); } return true; diff --git a/dGame/dComponents/PhysicsComponent.cpp b/dGame/dComponents/PhysicsComponent.cpp index 16722c541..0d3a302b2 100644 --- a/dGame/dComponents/PhysicsComponent.cpp +++ b/dGame/dComponents/PhysicsComponent.cpp @@ -249,18 +249,11 @@ bool PhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& auto& info = reportInfo.info->PushDebug("Physics"); reportInfo.subCategory = &info; - auto& pos = info.PushDebug("Position"); - pos.PushDebug("x") = m_Position.x; - pos.PushDebug("y") = m_Position.y; - pos.PushDebug("z") = m_Position.z; - - auto& rot = info.PushDebug("Rotation"); - rot.PushDebug("w") = m_Rotation.w; - rot.PushDebug("x") = m_Rotation.x; - rot.PushDebug("y") = m_Rotation.y; - rot.PushDebug("z") = m_Rotation.z; - - info.PushDebug("CollisionGroup") = m_CollisionGroup; + auto& pos = info.PushDebug("Position").PushDebug(m_Position); + + auto& rot = info.PushDebug("Rotation").PushDebug(m_Rotation); + + info.PushDebug("Collision Group") = m_CollisionGroup; return true; } diff --git a/dGame/dComponents/PossessableComponent.cpp b/dGame/dComponents/PossessableComponent.cpp index 39e732c68..a38cf5ba9 100644 --- a/dGame/dComponents/PossessableComponent.cpp +++ b/dGame/dComponents/PossessableComponent.cpp @@ -10,7 +10,7 @@ PossessableComponent::PossessableComponent(Entity* parent, const int32_t compone m_AnimationFlag = static_cast(item.animationFlag); // Get the possession Type from the CDClient - auto query = CDClientDatabase::CreatePreppedStmt("SELECT possessionType, depossessOnHit FROM PossessableComponent WHERE id = ?;"); + auto query = CDClientDatabase::CreatePreppedStmt("SELECT possessionType, depossessOnHit, skillSet FROM PossessableComponent WHERE id = ?;"); query.bind(1, static_cast(componentID)); @@ -20,6 +20,7 @@ PossessableComponent::PossessableComponent(Entity* parent, const int32_t compone if (!result.eof()) { m_PossessionType = static_cast(result.getIntField("possessionType", 1)); // Default to Attached Visible m_DepossessOnHit = static_cast(result.getIntField("depossessOnHit", 0)); + m_SkillSet = result.getIntField("skillSet", 0); } else { m_PossessionType = ePossessionType::ATTACHED_VISIBLE; m_DepossessOnHit = false; diff --git a/dGame/dComponents/PossessableComponent.h b/dGame/dComponents/PossessableComponent.h index 64a9dd739..776d0fe61 100644 --- a/dGame/dComponents/PossessableComponent.h +++ b/dGame/dComponents/PossessableComponent.h @@ -55,6 +55,12 @@ class PossessableComponent final : public Component { */ bool GetDepossessOnHit() const { return m_DepossessOnHit; }; + /** + * Returns the skill set ID for this possessable (0 = no skill set) + * @return the skill set ID + */ + int32_t GetSkillSet() const { return m_SkillSet; }; + /** * Forcibly depossess the Entity */ @@ -118,4 +124,9 @@ class PossessableComponent final : public Component { * */ bool m_ItemSpawned = false; + + /** + * @brief Skill set ID from PossessableComponent CDClient table (0 = none) + */ + int32_t m_SkillSet = 0; }; diff --git a/dGame/dComponents/PossessorComponent.cpp b/dGame/dComponents/PossessorComponent.cpp index 88ac1c2af..a5a4fcdd6 100644 --- a/dGame/dComponents/PossessorComponent.cpp +++ b/dGame/dComponents/PossessorComponent.cpp @@ -1,11 +1,10 @@ #include "PossessorComponent.h" #include "PossessableComponent.h" #include "CharacterComponent.h" +#include "HavokVehiclePhysicsComponent.h" #include "EntityManager.h" #include "GameMessages.h" #include "eUnequippableActiveType.h" -#include "eControlScheme.h" -#include "eStateChangeType.h" PossessorComponent::PossessorComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_Possessable = LWOOBJID_EMPTY; @@ -42,21 +41,27 @@ void PossessorComponent::Mount(Entity* mount) { // Don't do anything if we are busy dismounting if (GetIsDismounting() || !mount) return; - GameMessages::SendSetMountInventoryID(m_Parent, mount->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS); auto* possessableComponent = mount->GetComponent(); if (possessableComponent) { possessableComponent->SetPossessor(m_Parent->GetObjectID()); SetPossessable(mount->GetObjectID()); SetPossessableType(possessableComponent->GetPossessionType()); + if (possessableComponent->GetSkillSet() != 0) { + GameMessages::UseSkillSet useSkillSet; + useSkillSet.target = m_Parent->GetObjectID(); + useSkillSet.possessedId = mount->GetObjectID(); + useSkillSet.setId = possessableComponent->GetSkillSet(); + useSkillSet.Send(m_Parent->GetSystemAddress()); + } } - auto characterComponent = m_Parent->GetComponent(); - if (characterComponent) characterComponent->SetIsRacing(true); - // GM's to send GameMessages::SendSetJetPackMode(m_Parent, false); - GameMessages::SendVehicleUnlockInput(mount->GetObjectID(), false, m_Parent->GetSystemAddress()); - GameMessages::SendSetStunned(m_Parent->GetObjectID(), eStateChangeType::PUSH, m_Parent->GetSystemAddress(), LWOOBJID_EMPTY, true, false, true, false, false, false, false, true, true, true, true, true, true, true, true, true); + if (mount->GetComponent()) { + auto characterComponent = m_Parent->GetComponent(); + if (characterComponent) characterComponent->SetIsRacing(true); + GameMessages::SendVehicleUnlockInput(mount->GetObjectID(), false, m_Parent->GetSystemAddress()); + } Game::entityManager->SerializeEntity(m_Parent); Game::entityManager->SerializeEntity(mount); @@ -72,13 +77,21 @@ void PossessorComponent::Dismount(Entity* mount, bool forceDismount) { if (possessableComponent) { possessableComponent->SetPossessor(LWOOBJID_EMPTY); if (forceDismount) possessableComponent->ForceDepossess(); + if (possessableComponent->GetSkillSet() != 0) { + GameMessages::UseSkillSet useSkillSet; + useSkillSet.target = m_Parent->GetObjectID(); + useSkillSet.possessedId = mount->GetObjectID(); + useSkillSet.setId = possessableComponent->GetSkillSet(); + useSkillSet.bRemove = true; + useSkillSet.Send(m_Parent->GetSystemAddress()); + } } Game::entityManager->SerializeEntity(m_Parent); Game::entityManager->SerializeEntity(mount); - auto characterComponent = m_Parent->GetComponent(); - if (characterComponent) characterComponent->SetIsRacing(false); + if (mount->GetComponent()) { + auto characterComponent = m_Parent->GetComponent(); + if (characterComponent) characterComponent->SetIsRacing(false); + } } - // Make sure we don't have wacky controls - GameMessages::SendSetPlayerControlScheme(m_Parent, eControlScheme::SCHEME_A); } diff --git a/dGame/dComponents/ProximityMonitorComponent.cpp b/dGame/dComponents/ProximityMonitorComponent.cpp index 14e440d58..ca48a429c 100644 --- a/dGame/dComponents/ProximityMonitorComponent.cpp +++ b/dGame/dComponents/ProximityMonitorComponent.cpp @@ -80,8 +80,9 @@ bool ProximityMonitorComponent::OnGetObjectReportInfo(GameMessages::GetObjectRep proxAmf.PushDebug("Position").PushDebug(entity->GetPosition()); proxAmf.PushDebug("Rotation").PushDebug(entity->GetRotation()); auto& collidingAmf = proxAmf.PushDebug("Colliding Objects"); + int i = 1; for (const auto& colliding : entity->GetCurrentlyCollidingObjects()) { - collidingAmf.PushDebug(std::to_string(colliding)); + collidingAmf.PushDebug(std::to_string(i++), "LWOOBJID") = std::to_string(colliding); } } diff --git a/dGame/dComponents/QuickBuildComponent.cpp b/dGame/dComponents/QuickBuildComponent.cpp index 52b2ddeee..e9a1512fb 100644 --- a/dGame/dComponents/QuickBuildComponent.cpp +++ b/dGame/dComponents/QuickBuildComponent.cpp @@ -576,25 +576,25 @@ bool QuickBuildComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf auto& quickbuild = reportInfo.info->PushDebug("Quick Build"); quickbuild.PushDebug("State") = StringifiedEnum::ToString(m_State).data(); quickbuild.PushDebug("Timer") = m_Timer; - quickbuild.PushDebug("TimerIncomplete") = m_TimerIncomplete; - quickbuild.PushDebug("ActivatorPosition").PushDebug(m_ActivatorPosition); - quickbuild.PushDebug("ActivatorId") = std::to_string(m_ActivatorId); - quickbuild.PushDebug("ShowResetEffect") = m_ShowResetEffect; + quickbuild.PushDebug("Timer Incomplete") = m_TimerIncomplete; + quickbuild.PushDebug("Activator Position").PushDebug(m_ActivatorPosition); + quickbuild.PushDebug("Activator ID", "LWOOBJID") = std::to_string(m_ActivatorId); + quickbuild.PushDebug("Show Reset Effect") = m_ShowResetEffect; quickbuild.PushDebug("Taken") = m_Taken; - quickbuild.PushDebug("ResetTime") = m_ResetTime; - quickbuild.PushDebug("CompleteTime") = m_CompleteTime; - quickbuild.PushDebug("TakeImagination") = m_TakeImagination; + quickbuild.PushDebug("Reset Time") = m_ResetTime; + quickbuild.PushDebug("Complete Time") = m_CompleteTime; + quickbuild.PushDebug("Take Imagination") = m_TakeImagination; quickbuild.PushDebug("Interruptible") = m_Interruptible; - quickbuild.PushDebug("SelfActivator") = m_SelfActivator; - auto& modules = quickbuild.PushDebug("CustomModules"); + quickbuild.PushDebug("Self Activator") = m_SelfActivator; + auto& modules = quickbuild.PushDebug("Custom Modules"); for (const auto cmodule : m_CustomModules) modules.PushDebug("Module") = cmodule; - quickbuild.PushDebug("ActivityId") = m_ActivityId; - quickbuild.PushDebug("PostImaginationCost") = m_PostImaginationCost; - quickbuild.PushDebug("TimeBeforeSmash") = m_TimeBeforeSmash; - quickbuild.PushDebug("TimeBeforeDrain") = m_TimeBeforeDrain; - quickbuild.PushDebug("DrainedImagination") = m_DrainedImagination; - quickbuild.PushDebug("RepositionPlayer") = m_RepositionPlayer; - quickbuild.PushDebug("SoftTimer") = m_SoftTimer; - quickbuild.PushDebug("Builder") = std::to_string(m_Builder); + quickbuild.PushDebug("Activity Id") = m_ActivityId; + quickbuild.PushDebug("Post Imagination Cost") = m_PostImaginationCost; + quickbuild.PushDebug("Time Before Smash") = m_TimeBeforeSmash; + quickbuild.PushDebug("Time Before Drain") = m_TimeBeforeDrain; + quickbuild.PushDebug("Drained Imagination") = m_DrainedImagination; + quickbuild.PushDebug("Reposition Player") = m_RepositionPlayer; + quickbuild.PushDebug("Soft Timer") = m_SoftTimer; + quickbuild.PushDebug("Builder", "LWOOBJID") = std::to_string(m_Builder); return true; } diff --git a/dGame/dComponents/ScriptComponent.cpp b/dGame/dComponents/ScriptComponent.cpp index 2c9be34b2..d4a2a9d4e 100644 --- a/dGame/dComponents/ScriptComponent.cpp +++ b/dGame/dComponents/ScriptComponent.cpp @@ -16,7 +16,7 @@ ScriptComponent::ScriptComponent(Entity* parent, const int32_t componentID, cons m_ScriptName = scriptName; SetScript(scriptName); - RegisterMsg(&ScriptComponent::OnGetObjectReportInfo); + Component::RegisterMsg(&ScriptComponent::OnGetObjectReportInfo); } void ScriptComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { diff --git a/dGame/dComponents/ScriptComponent.h b/dGame/dComponents/ScriptComponent.h index e79b8e91e..723df7393 100644 --- a/dGame/dComponents/ScriptComponent.h +++ b/dGame/dComponents/ScriptComponent.h @@ -8,6 +8,9 @@ #include "CppScripts.h" #include "Component.h" +#include "GameMessages.h" +#include +#include #include #include "eReplicaComponentType.h" @@ -42,9 +45,22 @@ class ScriptComponent final : public Component { * @param scriptName the name of the script to find */ void SetScript(const std::string& scriptName); - + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + // Registers a message from a script to be listened for on the parent object + template + void RegisterMsg(ScriptClass* scriptThis, bool (ScriptClass::*scriptHandler)(Entity&, DerivedMsgType&)) { + static_assert(std::is_base_of_v, "DerivedMsgType must derive from GameMessages::GameMsg base class."); + const auto boundMsg = std::bind(scriptHandler, scriptThis, std::placeholders::_1, std::placeholders::_2); + auto* const parent = m_Parent; + const auto castWrapper = [parent, boundMsg](GameMessages::GameMsg& msg) { + return boundMsg(*parent, static_cast(msg)); + }; + DerivedMsgType msg; + m_Parent->RegisterMsg(msg.msgId, castWrapper); + } + private: /** diff --git a/dGame/dComponents/SimplePhysicsComponent.cpp b/dGame/dComponents/SimplePhysicsComponent.cpp index dcb870215..cb888459b 100644 --- a/dGame/dComponents/SimplePhysicsComponent.cpp +++ b/dGame/dComponents/SimplePhysicsComponent.cpp @@ -79,14 +79,8 @@ void SimplePhysicsComponent::SetPhysicsMotionState(uint32_t value) { bool SimplePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { PhysicsComponent::OnGetObjectReportInfo(reportInfo); auto& info = reportInfo.subCategory->PushDebug("Simple Physics Info"); - auto& velocity = info.PushDebug("Velocity"); - velocity.PushDebug("x") = m_Velocity.x; - velocity.PushDebug("y") = m_Velocity.y; - velocity.PushDebug("z") = m_Velocity.z; - auto& angularVelocity = info.PushDebug("Angular Velocity"); - angularVelocity.PushDebug("x") = m_AngularVelocity.x; - angularVelocity.PushDebug("y") = m_AngularVelocity.y; - angularVelocity.PushDebug("z") = m_AngularVelocity.z; + info.PushDebug("Velocity").PushDebug(m_Velocity); + info.PushDebug("Angular Velocity").PushDebug(m_AngularVelocity); info.PushDebug("Physics Motion State") = m_PhysicsMotionState; info.PushDebug("Climbable Type") = StringifiedEnum::ToString(m_ClimbableType).data(); return true; diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index e7aa97f96..1918ca2ef 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -159,6 +159,10 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System InventoryComponent* inv = entity->GetComponent(); if (inv) { + // Clear server-side skill state so AddItemSkills sends fresh AddSkill + // packets to the now-ready client. Skills sent during entity construction + // (Serialize) arrive before LWOSkillComponent is initialized and are dropped. + inv->ClearSkills(); auto items = inv->GetEquippedItems(); for (auto pair : items) { const auto item = pair.second; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 6b57e177b..f40e79a45 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -366,18 +366,19 @@ void GameMessages::SendResetMissions(Entity* entity, const SystemAddress& sysAdd void GameMessages::SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint, int iIndex, int iDesiredWaypointIndex, int nextIndex, - eMovementPlatformState movementState) { + eMovementPlatformState movementState, bool special) { CBITSTREAM; CMSGHEADER; + const auto objID = entity->GetObjectID(); const auto lot = entity->GetLOT(); - if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308) { + if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308 || lot == 9483) { iDesiredWaypointIndex = (lot == 11306 || lot == 11308) ? 1 : 0; - iIndex = 0; - nextIndex = 0; + iIndex = lot == 9483 ? 1 : 0; + nextIndex = lot == 9483 && !special ? 1 : 0; bStopAtDesiredWaypoint = true; - movementState = eMovementPlatformState::Stationary; + movementState = lot == 9483 && !special ? eMovementPlatformState::Stopped : eMovementPlatformState::Stationary; } bitStream.Write(entity->GetObjectID()); @@ -3943,11 +3944,19 @@ void GameMessages::SendSetMountInventoryID(Entity* entity, const LWOOBJID& objec CMSGHEADER; bitStream.Write(entity->GetObjectID()); bitStream.Write(MessageType::Game::SET_MOUNT_INVENTORY_ID); - bitStream.Write(objectID); + bitStream.Write(objectID != LWOOBJID_EMPTY); + if (objectID != LWOOBJID_EMPTY) bitStream.Write(objectID); SEND_PACKET_BROADCAST; } +void GameMessages::UseSkillSet::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bRemove); + bitStream.Write(possessedId != LWOOBJID_EMPTY); + if (possessedId != LWOOBJID_EMPTY) bitStream.Write(possessedId); + bitStream.Write(setId != -1); + if (setId != -1) bitStream.Write(setId); +} void GameMessages::HandleDismountComplete(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { // Get the objectID from the bitstream @@ -3983,9 +3992,6 @@ void GameMessages::HandleDismountComplete(RakNet::BitStream& inStream, Entity* e // Update the entity that was possessing Game::entityManager->SerializeEntity(entity); - - // We aren't mounted so remove the stun - GameMessages::SendSetStunned(entity->GetObjectID(), eStateChangeType::POP, UNASSIGNED_SYSTEM_ADDRESS, LWOOBJID_EMPTY, true, false, true, false, false, false, false, true, true, true, true, true, true, true, true, true); } } } @@ -3993,10 +3999,14 @@ void GameMessages::HandleDismountComplete(RakNet::BitStream& inStream, Entity* e void GameMessages::HandleAcknowledgePossession(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { Game::entityManager->SerializeEntity(entity); - LWOOBJID objectId{}; - inStream.Read(objectId); - auto* mount = Game::entityManager->GetEntity(objectId); - if (mount) Game::entityManager->SerializeEntity(mount); + bool hasObjectId{}; + inStream.Read(hasObjectId); + if (hasObjectId) { + LWOOBJID objectId{}; + inStream.Read(objectId); + auto* mount = Game::entityManager->GetEntity(objectId); + if (mount) Game::entityManager->SerializeEntity(mount); + } } //Racing @@ -6484,8 +6494,8 @@ namespace GameMessages { } void TeamPickupItem::Serialize(RakNet::BitStream& stream) const { - stream.Write(lootID); - stream.Write(lootOwnerID); + stream.Write(lootID); + stream.Write(lootOwnerID); } void ToggleGMInvis::Serialize(RakNet::BitStream& stream) const { diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 8937e0287..ce70a42c5 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -45,6 +45,7 @@ enum class BehaviorSlot : int32_t; enum class eVendorTransactionResult : uint32_t; enum class eReponseMoveItemBetweenInventoryTypeCode : int32_t; enum class eMissionState : int; +enum class AiState : uint32_t; enum class eCameraTargetCyclingMode : int32_t { ALLOW_CYCLE_TEAMMATES, @@ -103,9 +104,11 @@ namespace GameMessages { void SendPlayNDAudioEmitter(Entity* entity, const SystemAddress& sysAddr, std::string audioGUID); void SendStartPathing(Entity* entity); + + // special is for the FV tree platform, feature is complete if we just do that so meh void SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint = false, int iIndex = 0, int iDesiredWaypointIndex = 1, int nextIndex = 1, - eMovementPlatformState movementState = eMovementPlatformState::Moving); + eMovementPlatformState movementState = eMovementPlatformState::Moving, bool special = false); void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid = -1); void SendRestoreToPostLoadStats(Entity* entity, const SystemAddress& sysAddr); @@ -962,5 +965,28 @@ namespace GameMessages { LWOOBJID childID{}; }; + + struct UseSkillSet : public GameMsg { + UseSkillSet() : GameMsg(MessageType::Game::USE_SKILL_SET) {} + void Serialize(RakNet::BitStream& bitStream) const override; + + bool bRemove{}; + LWOOBJID possessedId{ LWOOBJID_EMPTY }; + int32_t setId{ -1 }; + }; + + struct ObjectLoaded : public GameMsg { + ObjectLoaded() : GameMsg(MessageType::Game::OBJECT_LOADED) {} + + LWOOBJID objectID{}; + LOT lot{}; + }; + + struct NotifyCombatAIStateChange : public GameMsg { + NotifyCombatAIStateChange() : GameMsg(MessageType::Game::NOTIFY_COMBAT_AI_STATE_CHANGE) {} + + AiState newState{}; + AiState prevState{}; + }; }; #endif // GAMEMESSAGES_H diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index bf1bcda46..bdd3c45ae 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -261,7 +261,7 @@ void SlashCommandHandler::Startup() { Command TestMapCommand{ .help = "Transfers you to the given zone", - .info = "Transfers you to the given zone by id and clone id. Add \"force\" to skip checking if the zone is accessible (this can softlock your character, though, if you e.g. try to teleport to Frostburgh).", + .info = "Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now.", .aliases = { "testmap", "tm" }, .handle = DEVGMCommands::TestMap, .requiredLevel = eGameMasterLevel::FORUM_MODERATOR @@ -468,7 +468,7 @@ void SlashCommandHandler::Startup() { Command InspectCommand{ .help = "Inspect an object", - .info = "Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See detailed usage in the DLU docs", + .info = "Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the IDs of all components the entity has. Use `localCharacter` or `zoneControl` to inspect your current character or the zone control object.", .aliases = { "inspect" }, .handle = DEVGMCommands::Inspect, .requiredLevel = eGameMasterLevel::DEVELOPER diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index a6a4e8239..faf5a40da 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -1307,7 +1307,8 @@ namespace DEVGMCommands { ChatPackets::SendSystemMessage(sysAddr, u"Requesting map change..."); LWOCLONEID cloneId = 0; - bool force = false; + LWOINSTANCEID instanceID{}; + std::string targetScene; const auto reqZoneOptional = GeneralUtils::TryParse(splitArgs[0]); if (!reqZoneOptional) { @@ -1317,29 +1318,34 @@ namespace DEVGMCommands { const LWOMAPID reqZone = reqZoneOptional.value(); if (splitArgs.size() > 1) { - auto index = 1; - - if (splitArgs[index] == "force") { - index++; - - force = true; + const auto cloneIdOptional = GeneralUtils::TryParse(splitArgs[1]); + if (!cloneIdOptional) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id."); + return; } - if (splitArgs.size() > index) { - const auto cloneIdOptional = GeneralUtils::TryParse(splitArgs[index]); - if (!cloneIdOptional) { - ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id."); + cloneId = cloneIdOptional.value(); + + if (splitArgs.size() > 2) { + const auto instanceIDVal = GeneralUtils::TryParse(splitArgs[2]); + if (!instanceIDVal) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid instance id."); return; } - cloneId = cloneIdOptional.value(); + + instanceID = instanceIDVal.value(); + } + + if (splitArgs.size() > 3) { + targetScene = splitArgs[3]; } } const auto objid = entity->GetObjectID(); - if (force || Game::zoneManager->CheckIfAccessibleZone(reqZone)) { // to prevent tomfoolery + if (Game::zoneManager->CheckIfAccessibleZone(reqZone)) { // to prevent tomfoolery - ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, reqZone, cloneId, false, [objid](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { + ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, reqZone, cloneId, false, [objid, targetScene](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { auto* entity = Game::entityManager->GetEntity(objid); if (!entity) return; @@ -1357,6 +1363,7 @@ namespace DEVGMCommands { entity->GetCharacter()->SetZoneID(zoneID); entity->GetCharacter()->SetZoneInstance(zoneInstance); entity->GetCharacter()->SetZoneClone(zoneClone); + entity->GetCharacter()->SetTargetScene(targetScene); entity->GetComponent()->SetLastRocketConfig(u""); } @@ -1769,7 +1776,15 @@ namespace DEVGMCommands { void Inspect(Entity* entity, const SystemAddress& sysAddr, const std::string args) { const auto splitArgs = GeneralUtils::SplitString(args, ' '); if (splitArgs.empty()) return; - const auto idParsed = GeneralUtils::TryParse(splitArgs[0]); + std::optional idIntermed; + if (splitArgs[0] == "zoneControl") { + idIntermed = 0x3FFF'FFFFFFFE; + } else if (splitArgs[0] == "localCharacter") { + idIntermed = entity->GetObjectID(); + } else { + idIntermed = GeneralUtils::TryParse(splitArgs[0]); + } + const auto idParsed = idIntermed; // First try to get the object by its ID if provided. // Second try to get the object by player name. diff --git a/dScripts/02_server/CMakeLists.txt b/dScripts/02_server/CMakeLists.txt index 8114b2267..048139a00 100644 --- a/dScripts/02_server/CMakeLists.txt +++ b/dScripts/02_server/CMakeLists.txt @@ -37,6 +37,7 @@ target_include_directories(dScriptsServerBase PUBLIC "." "Minigame" "Minigame/General" "Objects" + "Objects/Hatchlings" ) target_precompile_headers(dScriptsServerBase REUSE_FROM dScriptsBase) diff --git a/dScripts/02_server/DLU/CMakeLists.txt b/dScripts/02_server/DLU/CMakeLists.txt index fb257d3e6..3185df56c 100644 --- a/dScripts/02_server/DLU/CMakeLists.txt +++ b/dScripts/02_server/DLU/CMakeLists.txt @@ -1,3 +1,4 @@ set(DSCRIPTS_SOURCES_02_SERVER_DLU "DLUVanityTeleportingObject.cpp" + "RegisterWithZoneControl.cpp" PARENT_SCOPE) diff --git a/dScripts/02_server/DLU/RegisterWithZoneControl.cpp b/dScripts/02_server/DLU/RegisterWithZoneControl.cpp new file mode 100644 index 000000000..e588f240c --- /dev/null +++ b/dScripts/02_server/DLU/RegisterWithZoneControl.cpp @@ -0,0 +1,12 @@ +#include "RegisterWithZoneControl.h" + +#include "Entity.h" +#include "EntityManager.h" +#include "GameMessages.h" + +void RegisterWithZoneControl::OnStartup(Entity* self) { + GameMessages::ObjectLoaded objLoaded; + objLoaded.objectID = self->GetObjectID(); + objLoaded.lot = self->GetLOT(); + objLoaded.Send(Game::entityManager->GetZoneControlEntity()->GetObjectID()); +} diff --git a/dScripts/02_server/DLU/RegisterWithZoneControl.h b/dScripts/02_server/DLU/RegisterWithZoneControl.h new file mode 100644 index 000000000..dda89882b --- /dev/null +++ b/dScripts/02_server/DLU/RegisterWithZoneControl.h @@ -0,0 +1,14 @@ +// Darkflame Universe +// Copyright 2026 + +#ifndef REGISTERWITHZONECONTROL_H +#define REGISTERWITHZONECONTROL_H + +#include "CppScripts.h" + +class RegisterWithZoneControl : public CppScripts::Script { +public: + void OnStartup(Entity* self) override; +}; + +#endif //!REGISTERWITHZONECONTROL_H diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp index b3938fd45..869e09bae 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp @@ -16,6 +16,7 @@ #include "eReplicaComponentType.h" #include "RenderComponent.h" #include "PlayerManager.h" +#include "eStateChangeType.h" #include @@ -48,10 +49,30 @@ void BossSpiderQueenEnemyServer::OnStartup(Entity* self) { combat->SetStunImmune(true); m_CurrentBossStage = 1; - + ToggleAttacking(*self, false); + self->SetProximityRadius(65.0f, "AggroRadius"); // 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(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(u"player_count", playerCount); +} + void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { if (Game::zoneManager->GetZoneID().GetMapID() == instanceZoneID && killer) { for (const auto& player : PlayerManager::GetAllPlayers()) { @@ -71,6 +92,7 @@ void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { self->SetPosition({ 10000, 0, 10000 }); Game::entityManager->SerializeEntity(self); + ToggleAttacking(*self, false); controller->OnFireEventServerSide(self, "ClearProperty"); } @@ -634,3 +656,19 @@ float BossSpiderQueenEnemyServer::PlayAnimAndReturnTime(Entity* self, const std: return animTimer; } + +void BossSpiderQueenEnemyServer::ToggleAttacking(Entity& self, bool on) { + const auto stoppedFlag = self.GetVarAs(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 + } +} diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h index b59090002..0f975abe8 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h @@ -46,7 +46,10 @@ class BossSpiderQueenEnemyServer final : public CppScripts::Script { void OnTimerDone(Entity* self, std::string timerName) override; + void OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status); + private: + void ToggleAttacking(Entity& self, bool on); //Regular variables: DestroyableComponent* destroyable = nullptr; ControllablePhysicsComponent* controllable = nullptr; diff --git a/dScripts/02_server/Enemy/FV/CMakeLists.txt b/dScripts/02_server/Enemy/FV/CMakeLists.txt index 5146d8d84..af3be0b42 100644 --- a/dScripts/02_server/Enemy/FV/CMakeLists.txt +++ b/dScripts/02_server/Enemy/FV/CMakeLists.txt @@ -1,4 +1,5 @@ set(DSCRIPTS_SOURCES_02_SERVER_ENEMY_FV + "DragonRonin.cpp" "FvMaelstromCavalry.cpp" "FvMaelstromDragon.cpp" PARENT_SCOPE) diff --git a/dScripts/02_server/Enemy/FV/DragonRonin.cpp b/dScripts/02_server/Enemy/FV/DragonRonin.cpp new file mode 100644 index 000000000..64dcc20e5 --- /dev/null +++ b/dScripts/02_server/Enemy/FV/DragonRonin.cpp @@ -0,0 +1,6 @@ +#include "DragonRonin.h" + +void DragonRonin::OnStartup(Entity* self) { + self->SetVar(u"suicideTimer", 40.0f); + CountdownDestroyAI::OnStartup(self); +} diff --git a/dScripts/02_server/Enemy/FV/DragonRonin.h b/dScripts/02_server/Enemy/FV/DragonRonin.h new file mode 100644 index 000000000..1dd45c9c3 --- /dev/null +++ b/dScripts/02_server/Enemy/FV/DragonRonin.h @@ -0,0 +1,7 @@ +#pragma once +#include "CountdownDestroyAI.h" + +class DragonRonin : public CountdownDestroyAI { +public: + void OnStartup(Entity* self) override; +}; diff --git a/dScripts/02_server/Enemy/General/CMakeLists.txt b/dScripts/02_server/Enemy/General/CMakeLists.txt index 5486d2b02..c685d290e 100644 --- a/dScripts/02_server/Enemy/General/CMakeLists.txt +++ b/dScripts/02_server/Enemy/General/CMakeLists.txt @@ -1,6 +1,7 @@ set(DSCRIPTS_SOURCES_02_SERVER_ENEMY_GENERAL "BaseEnemyMech.cpp" "BaseEnemyApe.cpp" + "CountdownDestroyAI.cpp" "GfApeSmashingQB.cpp" "TreasureChestDragonServer.cpp" "EnemyNjBuff.cpp" diff --git a/dScripts/02_server/Enemy/General/CountdownDestroyAI.cpp b/dScripts/02_server/Enemy/General/CountdownDestroyAI.cpp new file mode 100644 index 000000000..3ca35340c --- /dev/null +++ b/dScripts/02_server/Enemy/General/CountdownDestroyAI.cpp @@ -0,0 +1,50 @@ +#include "CountdownDestroyAI.h" + +#include "BaseCombatAIComponent.h" +#include "ScriptComponent.h" + +void CountdownDestroyAI::OnStartup(Entity* self) { + CountdownStartup(*self); + auto* scriptComp = self->GetComponent(); + if (scriptComp) scriptComp->RegisterMsg(this, &CountdownDestroyAI::OnNotifyCombatAIStateChange); +} + +void CountdownDestroyAI::CountdownStartup(Entity& self) { + auto suicideTimer = self.GetVar(u"suicideTimer"); + if (suicideTimer == 0.0f) suicideTimer = 60; + self.AddTimer("Dead", suicideTimer); +} + +void CountdownDestroyAI::OnHit(Entity* self, Entity* attacker) { + if (!self->GetVar(u"ShouldBeDead")) return; + self->CancelTimer("IsBeingAttacked"); + self->AddTimer("Dead", 5.0f); +} + +void CountdownDestroyAI::OnTimerDone(Entity* self, std::string timerName) { + if (timerName == "Dead") { + self->SetVar(u"ShouldBeDead", true); + if (self->GetVar(u"Busy")) { + self->AddTimer("IsBeingAttacked", 5.0f); + } else { + self->Smash(); + } + } else if (timerName == "IsBeingAttacked") { + self->Smash(); + } +} + +bool CountdownDestroyAI::OnNotifyCombatAIStateChange(Entity& self, GameMessages::NotifyCombatAIStateChange& notifyMsg) { + const auto curState = notifyMsg.newState; + if (curState == AiState::dead) return true; + + if (curState == AiState::aggro || curState == AiState::tether) { + self.SetVar(u"Busy", true); + } else { + self.SetVar(u"Busy", false); + if (self.GetVar(u"ShouldBeDead")) { + self.Smash(); + } + } + return true; +} diff --git a/dScripts/02_server/Enemy/General/CountdownDestroyAI.h b/dScripts/02_server/Enemy/General/CountdownDestroyAI.h new file mode 100644 index 000000000..cdcb76460 --- /dev/null +++ b/dScripts/02_server/Enemy/General/CountdownDestroyAI.h @@ -0,0 +1,13 @@ +#pragma once +#include "CppScripts.h" + +#include "GameMessages.h" + +class CountdownDestroyAI : public CppScripts::Script { +public: + void OnStartup(Entity* self) override; + void CountdownStartup(Entity& self); + void OnHit(Entity* self, Entity* attacker) override; + void OnTimerDone(Entity* self, std::string timerName) override; + bool OnNotifyCombatAIStateChange(Entity& self, GameMessages::NotifyCombatAIStateChange& msg); +}; diff --git a/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp b/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp index a02856ef7..16d805eb7 100644 --- a/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp +++ b/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp @@ -14,7 +14,7 @@ void VisToggleNotifierServer::OnMissionDialogueOK(Entity* self, Entity* target, auto spawners = Game::zoneManager->GetSpawnersByName(itr->second); if (spawners.empty()) return; for (const auto spawner : spawners) { - auto spawnedObjIds = spawner->GetSpawnedObjectIDs(); + const auto& spawnedObjIds = spawner->GetSpawnedObjectIDs(); for (const auto& objId : spawnedObjIds) { GameMessages::SendNotifyClientObject(objId, u"SetVisibility", visible); } diff --git a/dScripts/02_server/Map/njhub/CMakeLists.txt b/dScripts/02_server/Map/njhub/CMakeLists.txt index 367d46477..b2d0ba7f8 100644 --- a/dScripts/02_server/Map/njhub/CMakeLists.txt +++ b/dScripts/02_server/Map/njhub/CMakeLists.txt @@ -6,6 +6,7 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_NJHUB "EnemySkeletonSpawner.cpp" "FallingTile.cpp" "FlameJetServer.cpp" + "LightningOrbServer.cpp" "ImaginationShrineServer.cpp" "Lieutenant.cpp" "MonCoreNookDoors.cpp" diff --git a/dScripts/02_server/Map/njhub/LightningOrbServer.cpp b/dScripts/02_server/Map/njhub/LightningOrbServer.cpp new file mode 100644 index 000000000..e686d540d --- /dev/null +++ b/dScripts/02_server/Map/njhub/LightningOrbServer.cpp @@ -0,0 +1,12 @@ +#include "LightningOrbServer.h" + +void LightningOrbServer::OnCollisionPhantom(Entity* self, Entity* target) { + GameMessages::GetPosition playerPos; + playerPos.Send(target->GetObjectID()); + GameMessages::GetPosition selfPos; + selfPos.Send(self->GetObjectID()); + const NiPoint3 newVec((playerPos.pos.x - selfPos.pos.x) * 2.5, 15, (playerPos.pos.z - selfPos.pos.z) * 2.5); + // ahhhh aron said to put a TODO here moving platforms don't work lol. disable this so people can actually do the puzzle + // GameMessages::SendKnockback(target->GetObjectID(), self->GetObjectID(), self->GetObjectID(), 0, newVec); + // GameMessages::SendPlayFXEffect(target->GetObjectID(), -1, u"knockback", "knockback"); +} diff --git a/dScripts/02_server/Map/njhub/LightningOrbServer.h b/dScripts/02_server/Map/njhub/LightningOrbServer.h new file mode 100644 index 000000000..962d7b6a1 --- /dev/null +++ b/dScripts/02_server/Map/njhub/LightningOrbServer.h @@ -0,0 +1,8 @@ +#pragma once +#include "CppScripts.h" + +class LightningOrbServer : public CppScripts::Script +{ +public: + void OnCollisionPhantom(Entity* self, Entity* target) override; +}; diff --git a/dScripts/02_server/Map/njhub/OldManNPC.cpp b/dScripts/02_server/Map/njhub/OldManNPC.cpp index 4d1f1056a..8a649389f 100644 --- a/dScripts/02_server/Map/njhub/OldManNPC.cpp +++ b/dScripts/02_server/Map/njhub/OldManNPC.cpp @@ -13,7 +13,6 @@ void ResetMissions(Entity& user) { } void OldManNPC::OnUse(Entity* self, Entity* user) { - LOG(""); const auto* const missionComponent = user->GetComponent(); if (!missionComponent) return; @@ -24,7 +23,6 @@ void OldManNPC::OnUse(Entity* self, Entity* user) { } const auto missionState = mission->GetMissionState(); - LOG("mission state %i", missionState); if (missionState == eMissionState::AVAILABLE || missionState == eMissionState::COMPLETE_AVAILABLE) { ResetMissions(*user); } diff --git a/dScripts/02_server/Objects/CMakeLists.txt b/dScripts/02_server/Objects/CMakeLists.txt index 1b96d79fc..da7e9b60e 100644 --- a/dScripts/02_server/Objects/CMakeLists.txt +++ b/dScripts/02_server/Objects/CMakeLists.txt @@ -1,4 +1,11 @@ +add_subdirectory(Hatchlings) + set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS "AgSurvivalBuffStation.cpp" - "StinkyFishTarget.cpp" - PARENT_SCOPE) + "StinkyFishTarget.cpp") + +foreach(file ${DSCRIPTS_SOURCES_02_SERVER_OBJECTS_HATCHLINGS}) + set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS ${DSCRIPTS_SOURCES_02_SERVER_OBJECTS} "Hatchlings/${file}") +endforeach() + +set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS ${DSCRIPTS_SOURCES_02_SERVER_OBJECTS} PARENT_SCOPE) diff --git a/dScripts/02_server/Objects/Hatchlings/CMakeLists.txt b/dScripts/02_server/Objects/Hatchlings/CMakeLists.txt new file mode 100644 index 000000000..0655ec644 --- /dev/null +++ b/dScripts/02_server/Objects/Hatchlings/CMakeLists.txt @@ -0,0 +1,3 @@ +set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS_HATCHLINGS + "HatchlingPets.cpp" + PARENT_SCOPE) diff --git a/dScripts/02_server/Objects/Hatchlings/HatchlingPets.cpp b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.cpp new file mode 100644 index 000000000..bc29ae8d5 --- /dev/null +++ b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.cpp @@ -0,0 +1,83 @@ +#include "HatchlingPets.h" + +#include "Entity.h" +#include "MovementAIComponent.h" + +void HatchlingPets::OnStartup(Entity* self) { + self->SetVar(u"follow", false); + + self->SetProximityRadius(5, "StopFollow"); + self->SetProximityRadius(15, "Wander"); + self->SetProximityRadius(50, "Teleport"); + + Wander(*self, *self->GetOwner()); + self->AddComponent(-1, MovementAIInfo{ .wanderRadius = 2.5f }); +} + +void HatchlingPets::OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) { + auto* const parent = self->GetOwner(); + if (!entering || !entering->IsPlayer() || parent->GetObjectID() != entering->GetObjectID()) return; + + if (name == "StopFollow") { + if (status == "ENTER") { + if (self->GetVar(u"follow")) { + const auto randomWanderTime = GeneralUtils::GenerateRandomNumber(4, 9); + self->AddTimer("StartWander", randomWanderTime); + // stop following the player + auto* const movementAI = self->GetComponent(); + if (movementAI) { + movementAI->Stop(); + movementAI->FollowTarget(LWOOBJID_EMPTY); + } + self->SetVar(u"follow", false); + } + } + } else if (name == "Wander") { + if (status == "LEAVE") { + self->CancelAllTimers(); + // follow the player + auto* const movementAI = self->GetComponent(); + if (movementAI) { + movementAI->Stop(); + movementAI->FollowTarget(entering->GetObjectID()); + } + self->SetVar(u"follow", true); + } + } else if (name == "Teleport") { + if (status == "LEAVE") { + // stop following the player + auto* const movementAI = self->GetComponent(); + if (movementAI) { + movementAI->Stop(); + movementAI->FollowTarget(LWOOBJID_EMPTY); + } + GameMessages::GetPosition getPos; + getPos.Send(entering->GetObjectID()); + getPos.pos.z += 5.0f; + self->SetPosition(getPos.pos); + Game::entityManager->SerializeEntity(*self); + } + } +} + +void HatchlingPets::OnTimerDone(Entity* self, std::string timerName) { + if (timerName == "StartWander") { + Wander(*self, *self->GetOwner()); + } +} + +void HatchlingPets::Wander(Entity& self, Entity& player) { + GameMessages::GetPosition getPos; + if (!getPos.Send(player.GetObjectID())) { + LOG("Failed to get position for %llu", player.GetObjectID()); + return; + } + + const auto xWander = GeneralUtils::GenerateRandomNumber(0, 20) - 10.0f; + const auto zWander = GeneralUtils::GenerateRandomNumber(0, 20) - 10.0f; + getPos.pos.x += xWander; + getPos.pos.z += zWander; + auto* const movementAI = self.GetComponent(); + if (movementAI) movementAI->SetDestination(getPos.pos); + self.AddTimer("StartWander", GeneralUtils::GenerateRandomNumber(4, 9)); +} diff --git a/dScripts/02_server/Objects/Hatchlings/HatchlingPets.h b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.h new file mode 100644 index 000000000..0dd2d5b6d --- /dev/null +++ b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.h @@ -0,0 +1,14 @@ +#ifndef HATCHLINGPETS_H +#define HATCHLINGPETS_H + +#include "CppScripts.h" +#include "NiPoint3.h" + +class HatchlingPets : public CppScripts::Script { + void OnStartup(Entity* self) override; + void OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) override; + void OnTimerDone(Entity* self, std::string timerName) override; + void Wander(Entity& self, Entity& player); +}; + +#endif //!HATCHLINGPETS_H diff --git a/dScripts/BaseConsoleTeleportServer.cpp b/dScripts/BaseConsoleTeleportServer.cpp index d172696c3..7392f7590 100644 --- a/dScripts/BaseConsoleTeleportServer.cpp +++ b/dScripts/BaseConsoleTeleportServer.cpp @@ -94,6 +94,9 @@ void BaseConsoleTeleportServer::TransferPlayer(Entity* self, Entity* player, int const auto& teleportZone = self->GetVar(u"transferZoneID"); + auto* const character = player->GetCharacter(); + if (character && self->HasVar(u"spawnPoint")) character->SetTargetScene(self->GetVarAsString(u"spawnPoint")); + auto* characterComponent = player->GetComponent(); if (characterComponent) characterComponent->SendToZone(GeneralUtils::TryParse(GeneralUtils::UTF16ToWTF8(teleportZone), 0)); diff --git a/dScripts/CppScripts.cpp b/dScripts/CppScripts.cpp index 6300716f4..0e03c9836 100644 --- a/dScripts/CppScripts.cpp +++ b/dScripts/CppScripts.cpp @@ -135,8 +135,11 @@ #include "FvMaelstromCavalry.h" #include "FvHorsemenTrigger.h" #include "FvFlyingCreviceDragon.h" +#include "FvDragonInstanceServer.h" #include "FvMaelstromDragon.h" +#include "DragonRonin.h" #include "FvDragonSmashingGolemQb.h" +#include "CountdownDestroyAI.h" #include "TreasureChestDragonServer.h" #include "InstanceExitTransferPlayerToLastNonInstance.h" #include "FvFreeGfNinjas.h" @@ -166,6 +169,7 @@ #include "AgSalutingNpcs.h" #include "BossSpiderQueenEnemyServer.h" #include "RockHydrantSmashable.h" +#include "HatchlingPets.h" // Misc Scripts #include "ExplodingAsset.h" @@ -273,6 +277,7 @@ #include "MonCoreNookDoors.h" #include "MonCoreSmashableDoors.h" #include "FlameJetServer.h" +#include "LightningOrbServer.h" #include "BurningTile.h" #include "NjEarthDragonPetServer.h" #include "NjEarthPetServer.h" @@ -339,6 +344,8 @@ #include "ImaginationBackPack.h" #include "NsWinterRaceServer.h" +#include "RegisterWithZoneControl.h" + #include #include #include @@ -421,6 +428,7 @@ namespace { {"scripts\\ai\\NS\\L_NS_CONCERT_INSTRUMENT_QB.lua", []() {return new NsConcertInstrument();}}, {"scripts\\ai\\NS\\L_NS_JONNY_FLAG_MISSION_SERVER.lua", []() {return new NsJohnnyMissionServer();}}, {"scripts\\02_server\\Objects\\L_STINKY_FISH_TARGET.lua", []() {return new StinkyFishTarget();}}, + {"scripts\\02_server\\Objects\\Hatchlings\\L_HATCHLING_PETS.lua", []() {return new HatchlingPets();}}, {"scripts\\zone\\PROPERTY\\NS\\L_ZONE_NS_PROPERTY.lua", []() {return new ZoneNsProperty();}}, {"scripts\\02_server\\Map\\Property\\NS_Med\\L_ZONE_NS_MED_PROPERTY.lua", []() {return new ZoneNsMedProperty();}}, {"scripts\\02_server\\Map\\NS\\L_NS_TOKEN_CONSOLE_SERVER.lua", []() {return new NsTokenConsoleServer();}}, @@ -486,7 +494,10 @@ namespace { {"scripts\\ai\\FV\\L_ACT_NINJA_TURRET_1.lua", []() {return new ActNinjaTurret();}}, {"scripts\\02_server\\Map\\FV\\L_FV_HORSEMEN_TRIGGER.lua", []() {return new FvHorsemenTrigger();}}, {"scripts\\ai\\FV\\L_FV_FLYING_CREVICE_DRAGON.lua", []() {return new FvFlyingCreviceDragon();}}, + {"scripts\\ai\\FV\\Dragon_Instance\\L_FV_DRAGON_INSTANCE_SERVER.lua", []() {return new FvDragonInstanceServer();}}, + {"scripts\\02_server\\Enemy\\FV\\L_FV_DRAGON_RONIN.lua", []() {return new DragonRonin();}}, {"scripts\\02_server\\Enemy\\FV\\L_FV_MAELSTROM_DRAGON.lua", []() {return new FvMaelstromDragon();}}, + {"scripts\\02_server\\Enemy\\General\\L_COUNTDOWN_DESTROY_AI.lua", []() {return new CountdownDestroyAI();}}, {"scripts\\ai\\FV\\L_FV_DRAGON_SMASHING_GOLEM_QB.lua", []() {return new FvDragonSmashingGolemQb();}}, {"scripts\\02_server\\Enemy\\General\\L_TREASURE_CHEST_DRAGON_SERVER.lua", []() {return new TreasureChestDragonServer();}}, {"scripts\\ai\\GENERAL\\L_INSTANCE_EXIT_TRANSFER_PLAYER_TO_LAST_NON_INSTANCE.lua", []() {return new InstanceExitTransferPlayerToLastNonInstance();}}, @@ -624,6 +635,7 @@ namespace { {"scripts\\02_server\\Map\\njhub\\L_MON_CORE_SMASHABLE_DOORS.lua", []() {return new MonCoreSmashableDoors();}}, {"scripts\\02_server\\Map\\njhub\\L_MON_CORE_SMASHABLE_DOORS.lua", []() {return new MonCoreSmashableDoors();}}, {"scripts\\02_server\\Map\\njhub\\L_FLAME_JET_SERVER.lua", []() {return new FlameJetServer();}}, + {"scripts\\02_server\\Map\\njhub\\L_LIGHTNING_ORB_SERVER.lua", []() {return new LightningOrbServer();}}, {"scripts\\02_server\\Map\\njhub\\L_BURNING_TILE.lua", []() {return new BurningTile();}}, {"scripts\\02_server\\Map\\njhub\\L_SPAWN_EARTH_PET_SERVER.lua", []() {return new NjEarthDragonPetServer();}}, {"scripts\\02_server\\Map\\njhub\\L_EARTH_PET_SERVER.lua", []() {return new NjEarthPetServer();}}, @@ -663,6 +675,7 @@ namespace { //WBL {"scripts\\zone\\LUPs\\WBL_generic_zone.lua", []() {return new WblGenericZone();}}, + {"scripts\\zone\\LUPs\\Moonbase Intro\\MOONBASE-INTRO_INTRO_CINEMATIC.lua", []() {return new WblGenericZone();}}, //Alpha {"scripts\\ai\\FV\\L_TRIGGER_GAS.lua", []() {return new TriggerGas();}}, @@ -708,7 +721,8 @@ namespace { {"scripts\\ai\\RACING\\OBJECTS\\VEHICLE_DEATH_TRIGGER_WATER_SERVER.lua", []() {return new VehicleDeathTriggerWaterServer();}}, {"scripts\\equipmenttriggers\\L_TRIAL_FACTION_ARMOR_SERVER.lua", []() {return new TrialFactionArmorServer();}}, {"scripts\\equipmenttriggers\\ImaginationBackPack.lua", []() {return new ImaginationBackPack();}}, - + {"scripts\\ai\\MINIGAME\\SG_GF\\SERVER\\SG_CANNON_INSTANCE_ACTOR.lua", [](){return new RegisterWithZoneControl();}}, + {"scripts\\ai\\MINIGAME\\SG_GF\\SERVER\\SG_CANNON_INSTANCE_EFFECT.lua", [](){return new RegisterWithZoneControl();}}, }; std::set g_ExcludedScripts = { @@ -732,6 +746,11 @@ namespace { "scripts\\zone\\LUPs\\RobotCity Intro\\WBL_RCIntro_InfectedCitizen.lua", "scripts\\ai\\MINIGAME\\SIEGE\\OBJECTS\\ATTACKER_BOUNCER_SERVER.lua", "scripts\\ai\\AG\\L_AG_ZONE_PLAYER.lua", + "scripts\\ai\\GENERAL\\L_NPC_GENERIC_MOVEMENT.lua", // Really old alpha script + "scripts\\zone\\LUPs\\DeepFreeze Intro\\WBL_Enemy_Beaver.lua", // Really old alpha script + "scripts\\ai\\GENERAL\\L_NPC_GENERIC_WANDER_SMALL.lua", // Really old alpha script + "scripts\\ai\\NP\\L_NPC_NP_OLD_MAN_SHERLAND.lua", // This NPC doesn't even exist in modern crux, the only place this is used... + "scripts\\02_server\\Map\\General\\L_SIMPLE_MOVER_SWITCH.lua", // This platform does not exist even when moved manually on a client }; }; @@ -745,7 +764,8 @@ CppScripts::Script* const CppScripts::GetScript(Entity* parent, const std::strin Script* script = itrTernary != scriptLoader.cend() ? itrTernary->second() : &InvalidToReturn; if (script == &InvalidToReturn && !scriptName.empty() && !g_ExcludedScripts.contains(scriptName)) { - LOG_DEBUG("LOT %i attempted to load CppScript for '%s', but returned InvalidScript.", parent->GetLOT(), scriptName.c_str()); + const auto [x, y, z] = parent->GetPosition(); + LOG_DEBUG("LOT %i at %f %f %f attempted to load CppScript for '%s', but returned InvalidScript.", parent->GetLOT(), x, y, z, scriptName.c_str()); } g_Scripts[scriptName] = script; diff --git a/dScripts/ai/FV/CMakeLists.txt b/dScripts/ai/FV/CMakeLists.txt index 535a02a66..eaa493c6c 100644 --- a/dScripts/ai/FV/CMakeLists.txt +++ b/dScripts/ai/FV/CMakeLists.txt @@ -18,7 +18,13 @@ set(DSCRIPTS_SOURCES_AI_FV "FvMaelstromGeyser.cpp" "TriggerGas.cpp") +add_subdirectory(Dragon_Instance) + +foreach(file ${DSCRIPTS_SOURCES_AI_FV_DRAGON_INSTANCE}) + set(DSCRIPTS_SOURCES_AI_FV ${DSCRIPTS_SOURCES_AI_FV} "Dragon_Instance/${file}") +endforeach() + add_library(dScriptsAiFV OBJECT ${DSCRIPTS_SOURCES_AI_FV}) -target_include_directories(dScriptsAiFV PUBLIC ".") +target_include_directories(dScriptsAiFV PUBLIC "." "Dragon_Instance") target_precompile_headers(dScriptsAiFV REUSE_FROM dScriptsBase) diff --git a/dScripts/ai/FV/Dragon_Instance/CMakeLists.txt b/dScripts/ai/FV/Dragon_Instance/CMakeLists.txt new file mode 100644 index 000000000..501c77ccb --- /dev/null +++ b/dScripts/ai/FV/Dragon_Instance/CMakeLists.txt @@ -0,0 +1,3 @@ +set(DSCRIPTS_SOURCES_AI_FV_DRAGON_INSTANCE + "FvDragonInstanceServer.cpp" + PARENT_SCOPE) diff --git a/dScripts/ai/FV/Dragon_Instance/FvDragonInstanceServer.cpp b/dScripts/ai/FV/Dragon_Instance/FvDragonInstanceServer.cpp new file mode 100644 index 000000000..3e63d6b0a --- /dev/null +++ b/dScripts/ai/FV/Dragon_Instance/FvDragonInstanceServer.cpp @@ -0,0 +1,14 @@ +#include "FvDragonInstanceServer.h" + +#include "Entity.h" +#include "DestroyableComponent.h" + +void FvDragonInstanceServer::OnPlayerLoaded(Entity* self, Entity* player) { + auto* const destComp = player->GetComponent(); + if (destComp) { + destComp->SetHealth(destComp->GetMaxHealth()); + destComp->SetArmor(destComp->GetMaxArmor()); + destComp->SetImagination(destComp->GetMaxImagination()); + Game::entityManager->SerializeEntity(player); + } +} diff --git a/dScripts/ai/FV/Dragon_Instance/FvDragonInstanceServer.h b/dScripts/ai/FV/Dragon_Instance/FvDragonInstanceServer.h new file mode 100644 index 000000000..5dffcae40 --- /dev/null +++ b/dScripts/ai/FV/Dragon_Instance/FvDragonInstanceServer.h @@ -0,0 +1,7 @@ +#pragma once +#include "CppScripts.h" + +class FvDragonInstanceServer : public CppScripts::Script { +public: + void OnPlayerLoaded(Entity* self, Entity* player) override; +}; diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 8de1d4e55..e4d324f1e 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1019,6 +1019,7 @@ void HandlePacket(Packet* packet) { if (user) { Character* c = user->GetLastUsedChar(); if (c != nullptr) { + if (Game::entityManager->GetEntity(c->GetObjectID())) return; std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName()); Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress); diff --git a/dZoneManager/Spawner.cpp b/dZoneManager/Spawner.cpp index cccdfff07..e70458424 100644 --- a/dZoneManager/Spawner.cpp +++ b/dZoneManager/Spawner.cpp @@ -6,8 +6,10 @@ #include #include "GeneralUtils.h" #include "dZoneManager.h" +#include +#include -Spawner::Spawner(const SpawnerInfo info) { +Spawner::Spawner(const SpawnerInfo& info) { m_Info = info; m_Active = m_Info.activeOnLoad && info.spawnActivator; m_EntityInfo = EntityInfo(); @@ -62,10 +64,6 @@ Spawner::Spawner(const SpawnerInfo info) { } } -Spawner::~Spawner() { - -} - Entity* Spawner::Spawn() { std::vector freeNodes; for (SpawnerNode* node : m_Info.nodes) { @@ -77,9 +75,25 @@ Entity* Spawner::Spawn() { return Spawn(freeNodes); } -Entity* Spawner::Spawn(std::vector freeNodes, const bool force) { - if (force || ((m_Entities.size() < m_Info.amountMaintained) && (freeNodes.size() > 0) && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) { - SpawnerNode* spawnNode = freeNodes[GeneralUtils::GenerateRandomNumber(0, freeNodes.size() - 1)]; +Entity* Spawner::Spawn(const std::vector& freeNodes, const bool force) { + Entity* spawnedEntity = nullptr; + 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(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_EntityInfo.pos = spawnNode->position; m_EntityInfo.rot = spawnNode->rotation; @@ -93,26 +107,24 @@ Entity* Spawner::Spawn(std::vector freeNodes, const bool force) { 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 }); - spawnNode->entities.push_back(rezdE->GetObjectID()); + m_Entities[spawnedEntity->GetObjectID()] = spawnNode; + spawnNode->entities.push_back(spawnedEntity->GetObjectID()); if (m_Entities.size() == m_Info.amountMaintained) { m_NeedsUpdate = false; } for (const auto& cb : m_EntitySpawnedCallbacks) { - cb(rezdE); + cb(spawnedEntity); } - - return rezdE; } - return nullptr; + return spawnedEntity; } void Spawner::AddSpawnedEntityDieCallback(std::function callback) { @@ -148,18 +160,18 @@ void Spawner::SoftReset() { m_NeedsUpdate = true; } -void Spawner::SetRespawnTime(float time) { +void Spawner::SetRespawnTime(const float time) { m_Info.respawnTime = time; for (size_t i = 0; i < m_WaitTimes.size(); ++i) { m_WaitTimes[i] = 0; - }; + } m_Start = true; m_NeedsUpdate = true; } -void Spawner::SetNumToMaintain(int32_t value) { +void Spawner::SetNumToMaintain(const int32_t value) { m_Info.amountMaintained = value; } @@ -177,15 +189,8 @@ void Spawner::Update(const float deltaTime) { return; } - if (!m_NeedsUpdate) return; - if (!m_Active) return; - //if (m_Info.noTimedSpawn) return; - if (m_Info.spawnsOnSmash) { - if (!m_SpawnSmashFoundGroup) { + if (!m_NeedsUpdate || !m_Active || m_Info.spawnsOnSmash) return; - } - return; - } for (size_t i = 0; i < m_WaitTimes.size(); ) { m_WaitTimes[i] += deltaTime; if (m_WaitTimes[i] >= m_Info.respawnTime) { @@ -198,22 +203,21 @@ void Spawner::Update(const float deltaTime) { } } -std::vector Spawner::GetSpawnedObjectIDs() const { +const std::vector Spawner::GetSpawnedObjectIDs() const { std::vector ids; 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); } return ids; } void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) { - for (std::function cb : m_SpawnedEntityDieCallbacks) { + for (const auto& cb : m_SpawnedEntityDieCallbacks) { cb(); } m_NeedsUpdate = true; - //m_RespawnTime = 10.0f; m_WaitTimes.push_back(0.0f); SpawnerNode* node; @@ -221,9 +225,7 @@ void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) { if (it != m_Entities.end()) node = it->second; else return; - if (!node) { - return; - } + if (!node) return; for (size_t i = 0; i < node->entities.size();) { 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; } diff --git a/dZoneManager/Spawner.h b/dZoneManager/Spawner.h index 3f01161d0..a5c9f3555 100644 --- a/dZoneManager/Spawner.h +++ b/dZoneManager/Spawner.h @@ -11,12 +11,41 @@ #include "LDFFormat.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 { + // This spawner nodes position in the world NiPoint3 position = NiPoint3Constant::ZERO; + // The rotation of this spawner in the world NiQuaternion rotation = QuatUtils::IDENTITY; + // This spawners nodes ID in this spawner network uint32_t nodeID = 0; + // The max number of entities that can be spawned by this node 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 entities; + // The config of all entities spawned by this node LwoNameValue config; }; @@ -45,11 +74,10 @@ struct SpawnerInfo { class Spawner { public: - Spawner(SpawnerInfo info); - ~Spawner(); + Spawner(const SpawnerInfo& info); Entity* Spawn(); - Entity* Spawn(std::vector freeNodes, bool force = false); + Entity* Spawn(const std::vector& freeNodes, bool force = false); void Update(float deltaTime); void NotifyOfEntityDeath(const LWOOBJID& objectID); void Activate(); @@ -57,16 +85,16 @@ class Spawner { int32_t GetAmountSpawned() { return m_AmountSpawned; }; std::string GetName() { return m_Info.name; }; std::vector GetGroups() { return m_Info.groups; }; - void AddSpawnedEntityDieCallback(std::function callback); - void AddEntitySpawnedCallback(std::function callback); - void SetSpawnLot(LOT lot); + void AddSpawnedEntityDieCallback(const std::function callback); + void AddEntitySpawnedCallback(const std::function callback); + void SetSpawnLot(const LOT lot); void Reset(); void DestroyAllEntities(); void SoftReset(); - void SetRespawnTime(float time); - void SetNumToMaintain(int32_t value); + void SetRespawnTime(const float time); + void SetNumToMaintain(const int32_t value); bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; }; - std::vector GetSpawnedObjectIDs() const; + const std::vector GetSpawnedObjectIDs() const; SpawnerInfo m_Info; bool m_Active = true; diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 893d9598b..a5a8fa973 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -101,36 +101,47 @@ void Zone::LoadZoneIntoMemory() { m_Paths.reserve(pathCount); for (uint32_t i = 0; i < pathCount; ++i) LoadPath(file); - for (Path path : m_Paths) { + for (const Path& path : m_Paths) { if (path.pathType != PathType::Spawner) continue; - SpawnerInfo info = SpawnerInfo(); - for (PathWaypoint waypoint : path.pathWaypoints) { + SpawnerInfo info{}; + for (size_t i = 0; i < path.pathWaypoints.size(); i++) { + const auto& waypoint = path.pathWaypoints[i]; SpawnerNode* node = new SpawnerNode(); node->position = waypoint.position; node->rotation = waypoint.rotation; node->nodeID = 0; - node->config = waypoint.config; + node->config = path.pathWaypoints[0].config; + // All spawner waypoints get the config data of the first waypoint, but then we + // overwrite settings on this waypoint if we have another one defined of the same name + if (i != 0) { + for (const auto& [key, value] : waypoint.config) { + node->config.ParseInsert(value->GetString()); + } + } for (const auto& data : waypoint.config | std::views::values) { if (!data) continue; 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") { - node->nodeMax = std::stoi(data->GetValueAsString()); + node->nodeMax = GeneralUtils::TryParse(data->GetValueAsString(), 0); } else if (data->GetKey() == u"groupID") { // Load object group - std::string groupStr = data->GetValueAsString(); - info.groups = GeneralUtils::SplitString(groupStr, ';'); + info.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';'); if (info.groups.back().empty()) info.groups.erase(info.groups.end() - 1); } 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(); } else if (data->GetKey() == u"spawner_name") { 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.templateID = path.spawner.spawnedLOT; diff --git a/docs/Commands.md b/docs/Commands.md index 1603cb5e9..9cfb99855 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -122,7 +122,7 @@ These commands are primarily for development and testing. The usage of many of t |leave-zone|`/leave-zone`|If you are in an instanced zone, transfers you to the closest main world. For example, if you are in an instance of Avant Gardens Survival or the Spider Queen Battle, you are sent to Avant Gardens. If you are in the Battle of Nimbus Station, you are sent to Nimbus Station. Aliases: `/leavezone`.|0| |resurrect|`/resurrect`|Resurrects the player.|8| |setminifig|`/setminifig `|Alters your player's minifig. Body part can be one of "Eyebrows", "Eyes", "HairColor", "HairStyle", "Pants", "LeftHand", "Mouth", "RightHand", "Shirt", or "Hands". Changing minifig parts could break the character so this command is limited to GMs.|1| -|testmap|`/testmap (force) (clone-id)`|Transfers you to the given zone by id and clone id. Add "force" to skip checking if the zone is accessible (this can softlock your character, though, if you e.g. try to teleport to Frostburgh). Aliases: `/tm`.|1| +|testmap|`/testmap (clone-id) (instance-id) (spawn-point)`|Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now. Aliases: `/tm`.|1| |reportproxphys|`/reportproxphys`|Prints to console the position and radius of proximity sensors.|9| |spawnphysicsverts|`/spawnphysicsverts`|Spawns a 1x1 brick at all vertices of phantom physics objects|8| |teleport|`/teleport (y) `|Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player. Aliases: `/tele`, `/tp`.|6| @@ -145,7 +145,7 @@ These commands are primarily for development and testing. The usage of many of t |getnavmeshheight|`/getnavmeshheight`|Display the navmesh height at your current position|8| |giveuscore|`/giveuscore `|Gives uscore|8| |gmadditem|`/gmadditem (count)`|Adds the given item to your inventory by id. Aliases: `/give`.|8| -|inspect|`/inspect (-m | -a | -s | -p | -f (faction) | -t)`|Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the IDs of all components the entity has. See detailed usage in the DLU docs|8| +|inspect|`/inspect (-m | -a | -s | -p | -f (faction) | -t)`|Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the IDs of all components the entity has. Use `localCharacter` or `zoneControl` to inspect your current character or the zone control object.|8| |inspect-despawn|`/inspect-despawn `|Finds the closest entity with the given component or LDF variable and despawns it. Alias: `/inspect-destroy`|8| |list-spawns|`/list-spawns`|Lists all the character spawn points in the zone. Additionally, this command will display the current scene that plays when the character lands in the next zone, if there is one. Aliases: `/listspawns`.|8| |locrow|`/locrow`|Prints your current position and rotation information to the console|8|