From 002aa896d8d1b810eeaf000ec0931fedd0771beb Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 19 Oct 2025 05:22:45 -0700 Subject: [PATCH 01/69] feat: debug information (#1915) --- dCommon/Amf3.h | 15 +++++ dGame/dComponents/BaseCombatAIComponent.cpp | 75 +++++++++++++++++++++ dGame/dComponents/BaseCombatAIComponent.h | 2 + dGame/dComponents/BouncerComponent.cpp | 69 +++++++++++++++++++ dGame/dComponents/BouncerComponent.h | 32 +++++++++ dGame/dComponents/CollectibleComponent.cpp | 34 ++++++++++ dGame/dComponents/CollectibleComponent.h | 4 +- dGame/dComponents/DestroyableComponent.cpp | 6 +- dGame/dComponents/GhostComponent.cpp | 14 ++++ dGame/dComponents/GhostComponent.h | 2 + dGame/dComponents/SwitchComponent.h | 4 ++ 11 files changed, 253 insertions(+), 4 deletions(-) diff --git a/dCommon/Amf3.h b/dCommon/Amf3.h index 9a34ad59..174ad814 100644 --- a/dCommon/Amf3.h +++ b/dCommon/Amf3.h @@ -374,6 +374,21 @@ public: return value->Insert("value", std::make_unique()); } + AMFArrayValue& PushDebug(const NiPoint3& point) { + PushDebug("X") = point.x; + PushDebug("Y") = point.y; + PushDebug("Z") = point.z; + return *this; + } + + AMFArrayValue& PushDebug(const NiQuaternion& rot) { + PushDebug("W") = rot.w; + PushDebug("X") = rot.x; + PushDebug("Y") = rot.y; + PushDebug("Z") = rot.z; + return *this; + } + private: /** * The associative portion. These values are key'd with strings to an AMFValue. diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index d264801f..73118e36 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -27,8 +27,13 @@ #include "CDComponentsRegistryTable.h" #include "CDPhysicsComponentTable.h" #include "dNavMesh.h" +#include "Amf3.h" BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { + { + using namespace GameMessages; + RegisterMsg(this, &BaseCombatAIComponent::MsgGetObjectReportInfo); + } m_Target = LWOOBJID_EMPTY; m_DirtyStateOrTarget = true; m_State = AiState::spawn; @@ -839,3 +844,73 @@ void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float valu SetThreat(threat, 0.0f); m_Target = LWOOBJID_EMPTY; } + +bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { + using enum AiState; + auto& reportMsg = static_cast(msg); + auto& cmptType = reportMsg.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); + // if (m_Target != LWOOBJID_EMPTY) { + // LWOGameMessages::ObjGetName nameMsg(m_CurrentTarget); + // SEND_GAMEOBJ_MSG(nameMsg); + // if (!nameMsg.msg.name.empty()) targetInfo.PushDebug("Name") = nameMsg.msg.name; + // } + + auto& roundInfo = cmptType.PushDebug("Round Info"); + // roundInfo.PushDebug("Combat Round Time") = m_CombatRoundLength; + // roundInfo.PushDebug("Minimum Time") = m_MinRoundLength; + // roundInfo.PushDebug("Maximum Time") = m_MaxRoundLength; + // roundInfo.PushDebug("Selected Time") = m_SelectedTime; + // 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; + } + cmptType.PushDebug("Current Combat State") = curState; + + //switch (m_CombatBehaviorType) { + // case 0: curState = "Passive"; break; + // case 1: curState = "Aggressive"; break; + // case 2: curState = "Passive (Turret)"; break; + // case 3: curState = "Aggressive (Turret)"; break; + // default: curState = "Unknown or Undefined"; break; + //} + //cmptType.PushDebug("Current Combat Behavior State") = curState; + + //switch (m_CombatRole) { + // case 0: curState = "Melee"; break; + // case 1: curState = "Ranged"; break; + // case 2: curState = "Support"; break; + // default: curState = "Unknown or Undefined"; break; + //} + //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("Hard Tether Radius") = m_HardTetherRadius; + cmptType.PushDebug("Soft Tether Radius") = m_SoftTetherRadius; + cmptType.PushDebug("Aggro Radius") = m_AggroRadius; + cmptType.PushDebug("Tether Speed") = m_TetherSpeed; + cmptType.PushDebug("Aggro Speed") = m_TetherSpeed; + // cmptType.PushDebug("Specified Min Range") = m_SpecificMinRange; + // cmptType.PushDebug("Specified Max Range") = m_SpecificMaxRange; + auto& threats = cmptType.PushDebug("Target Threats"); + for (const auto& [id, threat] : m_ThreatEntries) { + threats.PushDebug(std::to_string(id)) = threat; + } + + auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats"); + for (const auto& [id, threat] : m_ThreatEntries) { + ignoredThreats.PushDebug(std::to_string(id) + " - Time") = threat; + } + return true; +} diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 164b2ef5..009a96d2 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -234,6 +234,8 @@ public: // Ignore a threat for a certain amount of time void IgnoreThreat(const LWOOBJID target, const float time); + bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); + private: /** * Returns the current target or the target that currently is the largest threat to this entity diff --git a/dGame/dComponents/BouncerComponent.cpp b/dGame/dComponents/BouncerComponent.cpp index a7c7f1a8..3c535e43 100644 --- a/dGame/dComponents/BouncerComponent.cpp +++ b/dGame/dComponents/BouncerComponent.cpp @@ -8,15 +8,33 @@ #include "GameMessages.h" #include "BitStream.h" #include "eTriggerEventType.h" +#include "Amf3.h" BouncerComponent::BouncerComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_PetEnabled = false; m_PetBouncerEnabled = false; m_PetSwitchLoaded = false; + m_Destination = GeneralUtils::TryParse( + GeneralUtils::SplitString(m_Parent->GetVarAsString(u"bouncer_destination"), '\x1f')) + .value_or(NiPoint3Constant::ZERO); + m_Speed = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"bouncer_speed")).value_or(-1.0f); + m_UsesHighArc = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"bouncer_uses_high_arc")).value_or(false); + m_LockControls = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"lock_controls")).value_or(false); + m_IgnoreCollision = !GeneralUtils::TryParse(m_Parent->GetVarAsString(u"ignore_collision")).value_or(true); + m_StickLanding = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"stickLanding")).value_or(false); + m_UsesGroupName = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"uses_group_name")).value_or(false); + m_GroupName = m_Parent->GetVarAsString(u"grp_name"); + m_MinNumTargets = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"num_targets_to_activate")).value_or(1); + m_CinematicPath = m_Parent->GetVarAsString(u"attached_cinematic_path"); if (parent->GetLOT() == 7625) { LookupPetSwitch(); } + + { + using namespace GameMessages; + RegisterMsg(this, &BouncerComponent::MsgGetObjectReportInfo); + } } BouncerComponent::~BouncerComponent() { @@ -94,3 +112,54 @@ void BouncerComponent::LookupPetSwitch() { }); } } + +bool BouncerComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { + auto& reportMsg = static_cast(msg); + auto& cmptType = reportMsg.info->PushDebug("Bouncer"); + cmptType.PushDebug("Component ID") = GetComponentID(); + auto& destPos = cmptType.PushDebug("Destination Position"); + if (m_Destination != NiPoint3Constant::ZERO) { + destPos.PushDebug(m_Destination); + } else { + destPos.PushDebug("WARNING: Bouncer has no target position, is likely missing config data"); + } + + + if (m_Speed == -1.0f) { + cmptType.PushDebug("WARNING: Bouncer has no speed value, is likely missing config data"); + } else { + cmptType.PushDebug("Bounce Speed") = m_Speed; + } + cmptType.PushDebug("Bounce trajectory arc") = m_UsesHighArc ? "High Arc" : "Low Arc"; + cmptType.PushDebug("Collision Enabled") = m_IgnoreCollision; + cmptType.PushDebug("Stick Landing") = m_StickLanding; + cmptType.PushDebug("Locks character's controls") = m_LockControls; + if (!m_CinematicPath.empty()) cmptType.PushDebug("Cinematic Camera Path (plays during bounce)") = m_CinematicPath; + + auto* switchComponent = m_Parent->GetComponent(); + auto& respondsToFactions = cmptType.PushDebug("Responds to Factions"); + if (!switchComponent || switchComponent->GetFactionsToRespondTo().empty()) respondsToFactions.PushDebug("Faction 1"); + else { + for (const auto faction : switchComponent->GetFactionsToRespondTo()) { + respondsToFactions.PushDebug(("Faction " + std::to_string(faction))); + } + } + + cmptType.PushDebug("Uses a group name for interactions") = m_UsesGroupName; + if (!m_UsesGroupName) { + if (m_MinNumTargets > 1) { + cmptType.PushDebug("WARNING: Bouncer has a required number of objects to activate, but no group for interactions."); + } + + if (!m_GroupName.empty()) { + cmptType.PushDebug("WARNING: Has a group name for interactions , but is marked to not use that name."); + } + } else { + if (m_GroupName.empty()) { + cmptType.PushDebug("WARNING: Set to use a group name for inter actions, but no group name is assigned"); + } + cmptType.PushDebug("Number of interactions to activate bouncer") = m_MinNumTargets; + } + + return true; +} diff --git a/dGame/dComponents/BouncerComponent.h b/dGame/dComponents/BouncerComponent.h index 53ba26fa..b3221e12 100644 --- a/dGame/dComponents/BouncerComponent.h +++ b/dGame/dComponents/BouncerComponent.h @@ -51,6 +51,8 @@ public: */ void LookupPetSwitch(); + bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); + private: /** * Whether this bouncer needs to be activated by a pet @@ -66,6 +68,36 @@ private: * Whether the pet switch for this bouncer has been located */ bool m_PetSwitchLoaded; + + // The bouncer destination + NiPoint3 m_Destination; + + // The speed at which the player is bounced + float m_Speed{}; + + // Whether to use a high arc for the bounce trajectory + bool m_UsesHighArc{}; + + // Lock controls when bouncing + bool m_LockControls{}; + + // Ignore collision when bouncing + bool m_IgnoreCollision{}; + + // Stick the landing afterwards or let the player slide + bool m_StickLanding{}; + + // Whether or not there is a group name + bool m_UsesGroupName{}; + + // The group name for targets + std::string m_GroupName{}; + + // The number of targets to activate the bouncer + int32_t m_MinNumTargets{}; + + // The cinematic path to play during the bounce + std::string m_CinematicPath{}; }; #endif // BOUNCERCOMPONENT_H diff --git a/dGame/dComponents/CollectibleComponent.cpp b/dGame/dComponents/CollectibleComponent.cpp index f6ba25b2..fce32e93 100644 --- a/dGame/dComponents/CollectibleComponent.cpp +++ b/dGame/dComponents/CollectibleComponent.cpp @@ -1,5 +1,39 @@ #include "CollectibleComponent.h" +#include "MissionComponent.h" +#include "dServer.h" +#include "Amf3.h" + +CollectibleComponent::CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) : + Component(parentEntity, componentID), m_CollectibleId(collectibleId) { + using namespace GameMessages; + RegisterMsg(this, &CollectibleComponent::MsgGetObjectReportInfo); +} + void CollectibleComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) { outBitStream.Write(GetCollectibleId()); } + +bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { + auto& reportMsg = static_cast(msg); + auto& cmptType = reportMsg.info->PushDebug("Collectible"); + auto collectibleID = static_cast(m_CollectibleId) + static_cast(Game::server->GetZoneID() << 8); + + cmptType.PushDebug("Component ID") = GetComponentID(); + + cmptType.PushDebug("Collectible ID") = GetCollectibleId(); + cmptType.PushDebug("Mission Tracking ID (for save data)") = collectibleID; + + auto* localCharEntity = Game::entityManager->GetEntity(reportMsg.clientID); + bool collected = false; + if (localCharEntity) { + auto* missionComponent = localCharEntity->GetComponent(); + + if (m_CollectibleId != 0) { + collected = missionComponent->HasCollectible(collectibleID); + } + } + + cmptType.PushDebug("Has been collected") = collected; + return true; +} diff --git a/dGame/dComponents/CollectibleComponent.h b/dGame/dComponents/CollectibleComponent.h index d9356112..ba1a3f28 100644 --- a/dGame/dComponents/CollectibleComponent.h +++ b/dGame/dComponents/CollectibleComponent.h @@ -7,10 +7,12 @@ class CollectibleComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::COLLECTIBLE; - CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) : Component(parentEntity, componentID), m_CollectibleId(collectibleId) {} + CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId); int16_t GetCollectibleId() const { return m_CollectibleId; } void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) override; + + bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); private: int16_t m_CollectibleId = 0; }; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 0658757c..47aa0c90 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -1122,8 +1122,8 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { stats.PushDebug("Imagination") = m_iImagination; stats.PushDebug("Maximum Imagination") = m_fMaxImagination; stats.PushDebug("Damage Absorption Points") = m_DamageToAbsorb; - destroyableInfo.PushDebug("Is GM Immune") = m_IsGMImmune; - destroyableInfo.PushDebug("Is Shielded") = m_IsShielded; + stats.PushDebug("Is GM Immune") = m_IsGMImmune; + stats.PushDebug("Is Shielded") = m_IsShielded; destroyableInfo.PushDebug("Attacks To Block") = m_AttacksToBlock; destroyableInfo.PushDebug("Damage Reduction") = m_DamageReduction; std::stringstream factionsStream; @@ -1140,7 +1140,7 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { destroyableInfo.PushDebug("Enemy Factions") = factionsStream.str(); - destroyableInfo.PushDebug("Is Smashable") = m_IsSmashable; + destroyableInfo.PushDebug("Is A Smashable") = m_IsSmashable; destroyableInfo.PushDebug("Is Smashed") = m_IsSmashed; destroyableInfo.PushDebug("Is Module Assembly") = m_IsModuleAssembly; destroyableInfo.PushDebug("Explode Factor") = m_ExplodeFactor; diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index 4755a1f9..d86de72b 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -1,9 +1,14 @@ #include "GhostComponent.h" +#include "Amf3.h" +#include "GameMessages.h" + GhostComponent::GhostComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_GhostReferencePoint = NiPoint3Constant::ZERO; m_GhostOverridePoint = NiPoint3Constant::ZERO; m_GhostOverride = false; + + RegisterMsg(this, &GhostComponent::MsgGetObjectReportInfo); } GhostComponent::~GhostComponent() { @@ -55,3 +60,12 @@ bool GhostComponent::IsObserved(LWOOBJID id) { void GhostComponent::GhostEntity(LWOOBJID id) { m_ObservedEntities.erase(id); } + +bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { + auto& reportMsg = static_cast(msg); + auto& cmptType = reportMsg.info->PushDebug("Ghost"); + cmptType.PushDebug("Component ID") = GetComponentID(); + cmptType.PushDebug("Is GM Invis") = false; + + return true; +} diff --git a/dGame/dComponents/GhostComponent.h b/dGame/dComponents/GhostComponent.h index edf05c13..75ed3c9d 100644 --- a/dGame/dComponents/GhostComponent.h +++ b/dGame/dComponents/GhostComponent.h @@ -39,6 +39,8 @@ public: void GhostEntity(const LWOOBJID id); + bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); + private: NiPoint3 m_GhostReferencePoint; diff --git a/dGame/dComponents/SwitchComponent.h b/dGame/dComponents/SwitchComponent.h index ecbdeb73..755d134a 100644 --- a/dGame/dComponents/SwitchComponent.h +++ b/dGame/dComponents/SwitchComponent.h @@ -67,6 +67,10 @@ public: */ static SwitchComponent* GetClosestSwitch(NiPoint3 position); + const std::vector& GetFactionsToRespondTo() const { + return m_FactionsToRespondTo; + } + private: /** * A list of all pet switches. From 281d9762efb842f9cd78777dc3f93e3ed0e30caf Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 19 Oct 2025 05:23:54 -0700 Subject: [PATCH 02/69] fix: tac arc sorting and target acquisition (#1916) --- dGame/dBehaviors/TacArcBehavior.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/dGame/dBehaviors/TacArcBehavior.cpp b/dGame/dBehaviors/TacArcBehavior.cpp index 3e477896..d0bbad8e 100644 --- a/dGame/dBehaviors/TacArcBehavior.cpp +++ b/dGame/dBehaviors/TacArcBehavior.cpp @@ -114,7 +114,6 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS context->FilterTargets(validTargets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam); for (auto validTarget : validTargets) { - if (targets.size() >= this->m_maxTargets) break; if (std::find(targets.begin(), targets.end(), validTarget) != targets.end()) continue; if (validTarget->GetIsDead()) continue; @@ -147,13 +146,28 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS } } - std::sort(targets.begin(), targets.end(), [reference](Entity* a, Entity* b) { + std::sort(targets.begin(), targets.end(), [this, reference, combatAi](Entity* a, Entity* b) { const auto aDistance = Vector3::DistanceSquared(reference, a->GetPosition()); const auto bDistance = Vector3::DistanceSquared(reference, b->GetPosition()); - return aDistance > bDistance; + return aDistance < bDistance; }); + + if (m_useAttackPriority) { + // this should be using the attack priority column on the destroyable component + // We want targets with no threat level to remain the same order as above + // std::stable_sort(targets.begin(), targets.end(), [combatAi](Entity* a, Entity* b) { + // const auto aThreat = combatAi->GetThreat(a->GetObjectID()); + // const auto bThreat = combatAi->GetThreat(b->GetObjectID()); + + // If enabled for this behavior, prioritize threat over distance + // return aThreat > bThreat; + // }); + } + + // After we've sorted and found our closest targets, size the vector down in case there are too many + if (m_maxTargets > 0 && targets.size() > m_maxTargets) targets.resize(m_maxTargets); const auto hit = !targets.empty(); bitStream.Write(hit); From a70c365c23b93d3af08c18e98c24fbe1e65feff8 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 19 Oct 2025 12:00:14 -0700 Subject: [PATCH 03/69] feat banana (#1917) --- dGame/dUtilities/Loot.cpp | 2 ++ dScripts/ai/GF/GfBanana.cpp | 36 +++++++++--------------------- dScripts/ai/GF/GfBananaCluster.cpp | 22 ++++++++++++++++++ dScripts/ai/GF/GfBananaCluster.h | 1 + 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index 0b88f5af..d9384db8 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -365,6 +365,7 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::mapGetEntity(member); if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress()); @@ -377,6 +378,7 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::mapGetSystemAddress()); } diff --git a/dScripts/ai/GF/GfBanana.cpp b/dScripts/ai/GF/GfBanana.cpp index 93741d24..0b436396 100644 --- a/dScripts/ai/GF/GfBanana.cpp +++ b/dScripts/ai/GF/GfBanana.cpp @@ -55,36 +55,20 @@ void GfBanana::OnHit(Entity* self, Entity* attacker) { return; } + bananaEntity->Smash(LWOOBJID_EMPTY, eKillType::SILENT); - bananaEntity->SetPosition(bananaEntity->GetPosition() - NiPoint3Constant::UNIT_Y * 8); - - auto* bananaDestroyable = bananaEntity->GetComponent(); - - bananaDestroyable->SetHealth(0); - - bananaDestroyable->Smash(attacker->GetObjectID()); - - /* - auto position = self->GetPosition(); const auto rotation = self->GetRotation(); - - position.y += 12; - position.x -= rotation.GetRightVector().x * 5; - position.z -= rotation.GetRightVector().z * 5; - - EntityInfo info {}; - - info.pos = position; - info.rot = rotation; + EntityInfo info{}; info.lot = 6718; + info.pos = self->GetPosition(); + info.pos.y += 12; + info.pos.x -= QuatUtils::Right(rotation).x * 5; + info.pos.z -= QuatUtils::Right(rotation).z * 5; + info.rot = rotation; info.spawnerID = self->GetObjectID(); - - auto* entity = Game::entityManager->CreateEntity(info); - - Game::entityManager->ConstructEntity(entity, UNASSIGNED_SYSTEM_ADDRESS); - */ - - Game::entityManager->SerializeEntity(self); + info.settings = { new LDFData(u"motionType", 5) }; + auto* const newEn = Game::entityManager->CreateEntity(info, nullptr, self); + Game::entityManager->ConstructEntity(newEn); } void GfBanana::OnTimerDone(Entity* self, std::string timerName) { diff --git a/dScripts/ai/GF/GfBananaCluster.cpp b/dScripts/ai/GF/GfBananaCluster.cpp index 6e5e91db..f461c761 100644 --- a/dScripts/ai/GF/GfBananaCluster.cpp +++ b/dScripts/ai/GF/GfBananaCluster.cpp @@ -1,5 +1,9 @@ #include "GfBananaCluster.h" #include "Entity.h" +#include "dpWorld.h" +#include "dNavMesh.h" +#include "Loot.h" +#include "DestroyableComponent.h" void GfBananaCluster::OnStartup(Entity* self) { self->AddTimer("startup", 100); @@ -10,3 +14,21 @@ void GfBananaCluster::OnTimerDone(Entity* self, std::string timerName) { self->ScheduleKillAfterUpdate(nullptr); } } + +// Hack in banana loot dropping from tree area since it seemed to do that in live for some reason +void GfBananaCluster::OnHit(Entity* self, Entity* attacker) { + auto* parentEntity = self->GetParentEntity(); + GameMessages::GetPosition posMsg{}; + if (parentEntity) { + posMsg.target = parentEntity->GetObjectID(); + } + posMsg.Send(); + + const auto rotation = parentEntity ? parentEntity->GetRotation() : self->GetRotation(); + + if (dpWorld::GetNavMesh()) posMsg.pos.y = dpWorld::GetNavMesh()->GetHeightAtPoint(posMsg.pos) + 3.0f; + else posMsg.pos = posMsg.pos - (NiPoint3Constant::UNIT_Y * 8); + posMsg.pos.x -= QuatUtils::Right(rotation).x * 5; + posMsg.pos.z -= QuatUtils::Right(rotation).z * 5; + self->SetPosition(posMsg.pos); +} diff --git a/dScripts/ai/GF/GfBananaCluster.h b/dScripts/ai/GF/GfBananaCluster.h index 81bb8b0b..ceff708c 100644 --- a/dScripts/ai/GF/GfBananaCluster.h +++ b/dScripts/ai/GF/GfBananaCluster.h @@ -7,4 +7,5 @@ public: void OnStartup(Entity* self) override; void OnTimerDone(Entity* self, std::string timerName) override; + void OnHit(Entity* self, Entity* attacker) override; }; From 0dd504c803200cfd3238549ddffea8fc3e79d703 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:16:36 -0700 Subject: [PATCH 04/69] feat: behavior states (#1918) --- dGame/dComponents/ModelComponent.cpp | 19 +++++++------ dGame/dGameMessages/GameMessages.h | 5 ++++ dGame/dPropertyBehaviors/PropertyBehavior.cpp | 27 ++++++++++++++++--- dGame/dPropertyBehaviors/PropertyBehavior.h | 10 +++++-- dGame/dPropertyBehaviors/State.cpp | 4 +-- dGame/dPropertyBehaviors/State.h | 3 ++- dGame/dPropertyBehaviors/Strip.cpp | 27 +++++++++++++++---- dGame/dPropertyBehaviors/Strip.h | 5 ++-- 8 files changed, 77 insertions(+), 23 deletions(-) diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 85d206e2..2c975136 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -29,19 +29,22 @@ ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Comp bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { auto& reset = static_cast(msg); - for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset); - GameMessages::UnSmash unsmash; - unsmash.target = GetParent()->GetObjectID(); - unsmash.duration = 0.0f; - unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); + if (reset.bResetBehaviors) for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset); - m_Parent->SetPosition(m_OriginalPosition); - m_Parent->SetRotation(m_OriginalRotation); + if (reset.bUnSmash) { + GameMessages::UnSmash unsmash; + unsmash.target = GetParent()->GetObjectID(); + unsmash.duration = 0.0f; + unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); + m_NumActiveUnSmash = 0; + } + + if (reset.bResetPos) m_Parent->SetPosition(m_OriginalPosition); + if (reset.bResetRot) m_Parent->SetRotation(m_OriginalRotation); m_Parent->SetVelocity(NiPoint3Constant::ZERO); m_Speed = 3.0f; m_NumListeningInteract = 0; - m_NumActiveUnSmash = 0; m_NumActiveAttack = 0; GameMessages::SetFaction set{}; diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 1b52d67a..65349852 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -848,6 +848,11 @@ namespace GameMessages { struct ResetModelToDefaults : public GameMsg { ResetModelToDefaults() : GameMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS) {} + + bool bResetPos{ true }; + bool bResetRot{ true }; + bool bUnSmash{ true }; + bool bResetBehaviors{ true }; }; struct EmotePlayed : public GameMsg { diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index 0eb3f9df..d52a5380 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -5,6 +5,7 @@ #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" #include "ModelComponent.h" +#include "StringifiedEnum.h" #include @@ -178,13 +179,33 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) { } void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { - for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent); + auto& activeState = GetActiveState(); + UpdateResult updateResult{}; + activeState.Update(deltaTime, modelComponent, updateResult); + if (updateResult.newState.has_value() && updateResult.newState.value() != m_ActiveState) { + LOG("Behavior %llu is changing from state %s to %s", StringifiedEnum::ToString(m_ActiveState).data(), StringifiedEnum::ToString(updateResult.newState.value()).data()); + GameMessages::ResetModelToDefaults resetMsg{}; + resetMsg.bResetPos = false; + resetMsg.bResetRot = false; + resetMsg.bUnSmash = false; + resetMsg.bResetBehaviors = false; + modelComponent.OnResetModelToDefaults(resetMsg); + HandleMsg(resetMsg); + m_ActiveState = updateResult.newState.value(); + } } void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) { - for (auto& state : m_States | std::views::values) state.OnChatMessageReceived(sMessage); + auto& activeState = GetActiveState(); + activeState.OnChatMessageReceived(sMessage); } void PropertyBehavior::OnHit() { - for (auto& state : m_States | std::views::values) state.OnHit(); + auto& activeState = GetActiveState(); + activeState.OnHit(); +} + +State& PropertyBehavior::GetActiveState() { + DluAssert(m_States.contains(m_ActiveState)); + return m_States[m_ActiveState]; } diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.h b/dGame/dPropertyBehaviors/PropertyBehavior.h index f6a6be10..4fca613b 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.h +++ b/dGame/dPropertyBehaviors/PropertyBehavior.h @@ -1,18 +1,23 @@ #ifndef __PROPERTYBEHAVIOR__H__ #define __PROPERTYBEHAVIOR__H__ +#include "BehaviorStates.h" #include "State.h" +#include + namespace tinyxml2 { class XMLElement; } -enum class BehaviorState : uint32_t; - class AMFArrayValue; class BehaviorMessageBase; class ModelComponent; +struct UpdateResult { + std::optional newState; +}; + /** * Represents the Entity of a Property Behavior and holds data associated with the behavior */ @@ -45,6 +50,7 @@ public: void OnHit(); private: + State& GetActiveState(); // The current active behavior state. Behaviors can only be in ONE state at a time. BehaviorState m_ActiveState; diff --git a/dGame/dPropertyBehaviors/State.cpp b/dGame/dPropertyBehaviors/State.cpp index 5a2828e4..82d7ae0b 100644 --- a/dGame/dPropertyBehaviors/State.cpp +++ b/dGame/dPropertyBehaviors/State.cpp @@ -163,8 +163,8 @@ void State::Deserialize(const tinyxml2::XMLElement& state) { } } -void State::Update(float deltaTime, ModelComponent& modelComponent) { - for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent); +void State::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) { + for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent, updateResult); } void State::OnChatMessageReceived(const std::string& sMessage) { diff --git a/dGame/dPropertyBehaviors/State.h b/dGame/dPropertyBehaviors/State.h index a8d03ba7..436bb210 100644 --- a/dGame/dPropertyBehaviors/State.h +++ b/dGame/dPropertyBehaviors/State.h @@ -9,6 +9,7 @@ namespace tinyxml2 { class AMFArrayValue; class ModelComponent; +struct UpdateResult; class State { public: @@ -21,7 +22,7 @@ public: void Serialize(tinyxml2::XMLElement& state) const; void Deserialize(const tinyxml2::XMLElement& state); - void Update(float deltaTime, ModelComponent& modelComponent); + void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult); void OnChatMessageReceived(const std::string& sMessage); void OnHit(); diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index 330284a9..4ddc96cb 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -160,13 +160,14 @@ void Strip::SpawnDrop(LOT dropLOT, Entity& entity) { } } -void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { +void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) { auto& entity = *modelComponent.GetParent(); auto& nextAction = GetNextAction(); auto number = nextAction.GetValueParameterDouble(); auto valueStr = nextAction.GetValueParameterString(); auto numberAsInt = static_cast(number); auto nextActionType = GetNextAction().GetType(); + LOG_DEBUG("Processing Strip Action: %s with number %.2f and string %s", nextActionType.data(), number, valueStr.data()); // TODO replace with switch case and nextActionType with enum /* BEGIN Move */ @@ -223,7 +224,8 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); modelComponent.AddUnSmash(); - m_PausedTime = number; + // since it may take time for the message to relay to clients + m_PausedTime = number + 0.5f; } else if (nextActionType == "Wait") { m_PausedTime = number; } else if (nextActionType == "Chat") { @@ -258,6 +260,21 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup } /* END Gameplay */ + /* BEGIN StateMachine */ + else if (nextActionType == "ChangeStateHome") { + updateResult.newState = BehaviorState::HOME_STATE; + } else if (nextActionType == "ChangeStateCircle") { + updateResult.newState = BehaviorState::CIRCLE_STATE; + } else if (nextActionType == "ChangeStateSquare") { + updateResult.newState = BehaviorState::SQUARE_STATE; + } else if (nextActionType == "ChangeStateDiamond") { + updateResult.newState = BehaviorState::DIAMOND_STATE; + } else if (nextActionType == "ChangeStateTriangle") { + updateResult.newState = BehaviorState::TRIANGLE_STATE; + } else if (nextActionType == "ChangeStateStar") { + updateResult.newState = BehaviorState::STAR_STATE; + } + /* END StateMachine*/ else { static std::set g_WarnedActions; if (!g_WarnedActions.contains(nextActionType.data())) { @@ -330,7 +347,7 @@ bool Strip::CheckMovement(float deltaTime, ModelComponent& modelComponent) { return moveFinished; } -void Strip::Update(float deltaTime, ModelComponent& modelComponent) { +void Strip::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) { // No point in running a strip with only one action. // Strips are also designed to have 2 actions or more to run. if (!HasMinimumActions()) return; @@ -354,9 +371,9 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) { // Check for trigger blocks and if not a trigger block proc this blocks action if (m_NextActionIndex == 0) { + LOG("Behavior strip started %s", nextAction.GetType().data()); if (nextAction.GetType() == "OnInteract") { modelComponent.AddInteract(); - } else if (nextAction.GetType() == "OnChat") { // logic here if needed } else if (nextAction.GetType() == "OnAttack") { @@ -365,7 +382,7 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) { Game::entityManager->SerializeEntity(entity); m_WaitingForAction = true; } else { // should be a normal block - ProcNormalAction(deltaTime, modelComponent); + ProcNormalAction(deltaTime, modelComponent, updateResult); } } diff --git a/dGame/dPropertyBehaviors/Strip.h b/dGame/dPropertyBehaviors/Strip.h index 1a61afd5..330aafbc 100644 --- a/dGame/dPropertyBehaviors/Strip.h +++ b/dGame/dPropertyBehaviors/Strip.h @@ -12,6 +12,7 @@ namespace tinyxml2 { class AMFArrayValue; class ModelComponent; +struct UpdateResult; class Strip { public: @@ -33,9 +34,9 @@ public: // Checks the movement logic for whether or not to proceed // Returns true if the movement can continue, false if it needs to wait more. bool CheckMovement(float deltaTime, ModelComponent& modelComponent); - void Update(float deltaTime, ModelComponent& modelComponent); + void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult); void SpawnDrop(LOT dropLOT, Entity& entity); - void ProcNormalAction(float deltaTime, ModelComponent& modelComponent); + void ProcNormalAction(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult); void RemoveStates(ModelComponent& modelComponent) const; // 2 actions are required for strips to work From 83823fa64fe7a02d8f56209ec1b4b501833b2a0b Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:05:22 -0700 Subject: [PATCH 05/69] fix: resurrect not available for non-gms (#1919) --- dGame/dUtilities/SlashCommandHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index 218ccfa8..65ee97c6 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -1052,7 +1052,7 @@ void SlashCommandHandler::Startup() { .info = "Resurrects the player", .aliases = { "resurrect" }, .handle = GMZeroCommands::Resurrect, - .requiredLevel = eGameMasterLevel::CIVILIAN + .requiredLevel = eGameMasterLevel::DEVELOPER }; RegisterCommand(ResurrectCommand); From 46aac016fd73425dc0d1eb600740328fa562adaf Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:41:16 -0700 Subject: [PATCH 06/69] fix: unintended stopping (#1922) --- dGame/dPropertyBehaviors/PropertyBehavior.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index d52a5380..139b6d04 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -183,12 +183,11 @@ void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { UpdateResult updateResult{}; activeState.Update(deltaTime, modelComponent, updateResult); if (updateResult.newState.has_value() && updateResult.newState.value() != m_ActiveState) { - LOG("Behavior %llu is changing from state %s to %s", StringifiedEnum::ToString(m_ActiveState).data(), StringifiedEnum::ToString(updateResult.newState.value()).data()); + LOG("Behavior %llu is changing from state %s to %s", m_BehaviorId, StringifiedEnum::ToString(m_ActiveState).data(), StringifiedEnum::ToString(updateResult.newState.value()).data()); GameMessages::ResetModelToDefaults resetMsg{}; resetMsg.bResetPos = false; resetMsg.bResetRot = false; resetMsg.bUnSmash = false; - resetMsg.bResetBehaviors = false; modelComponent.OnResetModelToDefaults(resetMsg); HandleMsg(resetMsg); m_ActiveState = updateResult.newState.value(); From 6e545eb1b99ca83ad4ac934b6b81f5d9fd65bb0b Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:53:00 -0700 Subject: [PATCH 07/69] Update Loot.cpp (#1923) --- dGame/dUtilities/Loot.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index d9384db8..ae6f6177 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -355,7 +355,7 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::map(minCoins + GeneralUtils::GenerateRandomNumber(0, 1) * (maxCoins - minCoins)); - const auto droppedCoins = team ? std::ceil(coinRoll / team->members.size()) : coinRoll; + const auto droppedCoins = team ? std::ceil(static_cast(coinRoll) / team->members.size()) : coinRoll; if (team) { for (auto member : team->members) { GameMessages::DropClientLoot lootMsg{}; From 396dcb04658eef86ff00fffca968950c3f6c4d95 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 25 Oct 2025 12:53:49 -0700 Subject: [PATCH 08/69] feat: add logger feature to log on function entry and exit (#1924) * feat: add logger feature to log on function entry and exit * i didnt save the file --- dCommon/Logger.cpp | 14 ++++++++++++++ dCommon/Logger.h | 13 +++++++++++++ dGame/dBehaviors/AndBehavior.cpp | 2 ++ dGame/dBehaviors/Behavior.h | 2 ++ 4 files changed, 31 insertions(+) diff --git a/dCommon/Logger.cpp b/dCommon/Logger.cpp index 1888f1eb..0f31f541 100644 --- a/dCommon/Logger.cpp +++ b/dCommon/Logger.cpp @@ -96,3 +96,17 @@ bool Logger::GetLogToConsole() const { } return toReturn; } + +FuncEntry::FuncEntry(const char* funcName, const char* fileName, const uint32_t line) { + m_FuncName = funcName; + if (!m_FuncName) m_FuncName = "Unknown"; + m_Line = line; + m_FileName = fileName; + LOG("--> %s::%s:%i", m_FileName, m_FuncName, m_Line); +} + +FuncEntry::~FuncEntry() { + if (!m_FuncName || !m_FileName) return; + + LOG("<-- %s::%s:%i", m_FileName, m_FuncName, m_Line); +} diff --git a/dCommon/Logger.h b/dCommon/Logger.h index 3a1771e6..e7785a13 100644 --- a/dCommon/Logger.h +++ b/dCommon/Logger.h @@ -32,6 +32,19 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* path) { #define LOG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->Log(str_, message, ##__VA_ARGS__); } while(0) #define LOG_DEBUG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->LogDebug(str_, message, ##__VA_ARGS__); } while(0) +// Place this right at the start of a function. Will log a message when called and then once you leave the function. +#define LOG_ENTRY auto str_ = GetFileNameFromAbsolutePath(__FILE__); FuncEntry funcEntry_(__FUNCTION__, str_, __LINE__) + +class FuncEntry { +public: + FuncEntry(const char* funcName, const char* fileName, const uint32_t line); + ~FuncEntry(); +private: + const char* m_FuncName = nullptr; + const char* m_FileName = nullptr; + uint32_t m_Line = 0; +}; + // Writer class for writing data to files. class Writer { public: diff --git a/dGame/dBehaviors/AndBehavior.cpp b/dGame/dBehaviors/AndBehavior.cpp index ad986e37..c9d6e06e 100644 --- a/dGame/dBehaviors/AndBehavior.cpp +++ b/dGame/dBehaviors/AndBehavior.cpp @@ -10,7 +10,9 @@ void AndBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStream, } void AndBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitStream, const BehaviorBranchContext branch) { + LOG_ENTRY; for (auto* behavior : this->m_behaviors) { + LOG("%i calculating %i", m_behaviorId, behavior->GetBehaviorID()); behavior->Calculate(context, bitStream, branch); } } diff --git a/dGame/dBehaviors/Behavior.h b/dGame/dBehaviors/Behavior.h index f9867ffe..49d664c7 100644 --- a/dGame/dBehaviors/Behavior.h +++ b/dGame/dBehaviors/Behavior.h @@ -95,4 +95,6 @@ public: Behavior& operator=(const Behavior& other) = default; Behavior& operator=(Behavior&& other) = default; + + uint32_t GetBehaviorID() const { return m_behaviorId; } }; From ca60787055ee73f99cfa8591ee21b5468a7a3856 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 26 Oct 2025 01:01:21 -0700 Subject: [PATCH 09/69] fix: ffa -> shared loot for activities (#1925) --- dChatServer/TeamContainer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dChatServer/TeamContainer.cpp b/dChatServer/TeamContainer.cpp index bbdab0ac..536cc57f 100644 --- a/dChatServer/TeamContainer.cpp +++ b/dChatServer/TeamContainer.cpp @@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector members) { } } - newTeam->lootFlag = 1; + newTeam->lootFlag = 0; TeamStatusUpdate(newTeam); From eac50acfcc71c92d085193607d6278ed3b44cf80 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:29:03 -0800 Subject: [PATCH 10/69] fix: correct mission tracking (#1930) checked that live captures did not track achievements in this count --- dGame/dComponents/CharacterComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index a0c44961..54708226 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -515,12 +515,12 @@ void CharacterComponent::RocketUnEquip(Entity* player) { } void CharacterComponent::TrackMissionCompletion(bool isAchievement) { - UpdatePlayerStatistic(MissionsCompleted); - // Achievements are tracked separately for the zone if (isAchievement) { const auto mapID = Game::zoneManager->GetZoneID().GetMapID(); GetZoneStatisticsForMap(mapID).m_AchievementsCollected++; + } else { + UpdatePlayerStatistic(MissionsCompleted); } } From 96089a8d9a5a040b951df27411cb6e1c7e63d18c Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:29:11 -0800 Subject: [PATCH 11/69] fix: fb race activityid (#1929) --- dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp b/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp index 9c3cc009..9a7712a1 100644 --- a/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp @@ -19,7 +19,7 @@ void NsWinterRaceServer::OnStartup(Entity* self) { raceSet.push_back(make_unique>("Race_PathName", u"MainPath")); raceSet.push_back(make_unique>("Current_Lap", 1)); raceSet.push_back(make_unique>("Number_of_Laps", 3)); - raceSet.push_back(make_unique>("activityID", 42)); + raceSet.push_back(make_unique>("activityID", 60)); raceSet.push_back(make_unique>("Place_1", 100)); raceSet.push_back(make_unique>("Place_2", 90)); From 2fb16420f34ee2b441beaf7f082fdc3cff056953 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:30:02 -0800 Subject: [PATCH 12/69] fix: ape anchor not respawning (#1927) * fix: ape anchor not respawning * add return Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dGame/Entity.cpp | 10 ++++++++++ dGame/Entity.h | 1 + dGame/dGameMessages/GameMessages.h | 6 ++++++ dScripts/02_server/Enemy/General/BaseEnemyApe.cpp | 8 +++++++- dScripts/02_server/Enemy/General/BaseEnemyApe.h | 1 + dScripts/CppScripts.h | 2 ++ 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 7d8046e0..35e2cfdc 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -189,6 +189,10 @@ Entity::~Entity() { } if (m_ParentEntity) { + GameMessages::ChildRemoved removedMsg{}; + removedMsg.childID = m_ObjectID; + removedMsg.target = m_ParentEntity->GetObjectID(); + removedMsg.Send(); m_ParentEntity->RemoveChild(this); } } @@ -198,6 +202,7 @@ void Entity::Initialize() { RegisterMsg(this, &Entity::MsgDropClientLoot); RegisterMsg(this, &Entity::MsgGetFactionTokenType); RegisterMsg(this, &Entity::MsgPickupItem); + RegisterMsg(this, &Entity::MsgChildRemoved); /** * Setup trigger */ @@ -2352,3 +2357,8 @@ bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) { return true; } + +bool Entity::MsgChildRemoved(GameMessages::GameMsg& msg) { + GetScript()->OnChildRemoved(*this, static_cast(msg)); + return true; +} diff --git a/dGame/Entity.h b/dGame/Entity.h index 6d50efa7..5310cd0f 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -180,6 +180,7 @@ public: bool MsgGetFlag(GameMessages::GameMsg& msg); bool MsgGetFactionTokenType(GameMessages::GameMsg& msg); bool MsgPickupItem(GameMessages::GameMsg& msg); + bool MsgChildRemoved(GameMessages::GameMsg& msg); // This is expceted to never return nullptr, an assert checks this. CppScripts::Script* const GetScript() const; diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 65349852..9daae4b5 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -937,5 +937,11 @@ namespace GameMessages { LWOOBJID lootID{}; LWOOBJID lootOwnerID{}; }; + + struct ChildRemoved : public GameMsg { + ChildRemoved() : GameMsg(MessageType::Game::CHILD_REMOVED) {} + + LWOOBJID childID{}; + }; }; #endif // GAMEMESSAGES_H diff --git a/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp b/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp index db6754f3..9c71c284 100644 --- a/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp +++ b/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp @@ -92,7 +92,7 @@ void BaseEnemyApe::OnTimerDone(Entity* self, std::string timerName) { new LDFData(u"ape", self->GetObjectID()) }; - auto* anchor = Game::entityManager->CreateEntity(entityInfo); + auto* anchor = Game::entityManager->CreateEntity(entityInfo, nullptr, self); Game::entityManager->ConstructEntity(anchor); self->SetVar(u"QB", anchor->GetObjectID()); @@ -140,3 +140,9 @@ void BaseEnemyApe::StunApe(Entity* self, bool stunState) { self->SetBoolean(u"knockedOut", stunState); } } + +void BaseEnemyApe::OnChildRemoved(Entity& self, GameMessages::ChildRemoved& childRemoved) { + if (self.GetVar(u"QB") == childRemoved.childID) { + self.SetVar(u"QB", LWOOBJID_EMPTY); + } +} diff --git a/dScripts/02_server/Enemy/General/BaseEnemyApe.h b/dScripts/02_server/Enemy/General/BaseEnemyApe.h index 54a734bb..a31f1612 100644 --- a/dScripts/02_server/Enemy/General/BaseEnemyApe.h +++ b/dScripts/02_server/Enemy/General/BaseEnemyApe.h @@ -10,6 +10,7 @@ public: void OnTimerDone(Entity* self, std::string timerName) override; void OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) override; + void OnChildRemoved(Entity& self, GameMessages::ChildRemoved& childRemoved) override; private: static void StunApe(Entity* self, bool stunState); }; diff --git a/dScripts/CppScripts.h b/dScripts/CppScripts.h index eb483a11..5ba4cee2 100644 --- a/dScripts/CppScripts.h +++ b/dScripts/CppScripts.h @@ -383,6 +383,8 @@ namespace CppScripts { * @param fire The child info */ virtual void OnChildLoaded(Entity& self, GameMessages::ChildLoaded& childLoaded) {}; + + virtual void OnChildRemoved(Entity& self, GameMessages::ChildRemoved& childRemoved) {}; }; Script* const GetScript(Entity* parent, const std::string& scriptName); From 11d44ffb988963ae55eb2b4eb30dec40f3affaec Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 15 Nov 2025 18:43:33 -0600 Subject: [PATCH 13/69] feat: proper gminvs with ghosting (#1920) * feat: proper gminvis ghosting * address feedback --------- Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> --- dGame/Entity.cpp | 2 +- dGame/EntityManager.cpp | 44 +++++-------- dGame/dComponents/GhostComponent.cpp | 62 ++++++++++++++++++- dGame/dComponents/GhostComponent.h | 14 +++++ dGame/dGameMessages/GameMessages.cpp | 16 ++--- dGame/dGameMessages/GameMessages.h | 16 ++++- dGame/dUtilities/SlashCommandHandler.cpp | 4 +- .../SlashCommands/DEVGMCommands.cpp | 4 +- .../GMGreaterThanZeroCommands.cpp | 3 +- 9 files changed, 116 insertions(+), 49 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 35e2cfdc..f4ad52f3 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -504,7 +504,7 @@ void Entity::Initialize() { auto& systemAddress = m_Character->GetParentUser() ? m_Character->GetParentUser()->GetSystemAddress() : UNASSIGNED_SYSTEM_ADDRESS; AddComponent(characterID, m_Character, systemAddress)->LoadFromXml(m_Character->GetXMLDoc()); - AddComponent(characterID); + AddComponent(characterID)->LoadFromXml(m_Character->GetXMLDoc()); } const auto inventoryID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::INVENTORY); diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index 12be2e06..92258afc 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -361,16 +361,24 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr LOG("Attempted to construct null entity"); return; } + // Don't construct GM invisible entities unless it's for the GM themselves + // GMs can see other GMs if they are the same or lower level + GameMessages::GetGMInvis getGMInvisMsg; + getGMInvisMsg.Send(entity->GetObjectID()); + if (getGMInvisMsg.bGMInvis && sysAddr != entity->GetSystemAddress()) { + auto* toUser = UserManager::Instance()->GetUser(sysAddr); + if (!toUser) return; + auto* constructedUser = UserManager::Instance()->GetUser(entity->GetSystemAddress()); + if (!constructedUser) return; + if (toUser->GetMaxGMLevel() < constructedUser->GetMaxGMLevel()) return; + } if (entity->GetNetworkId() == 0) { uint16_t networkId; - if (!m_LostNetworkIds.empty()) { networkId = m_LostNetworkIds.top(); m_LostNetworkIds.pop(); - } else { - networkId = ++m_NetworkIdCounter; - } + } else networkId = ++m_NetworkIdCounter; entity->SetNetworkId(networkId); } @@ -379,10 +387,8 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr if (std::find(m_EntitiesToGhost.begin(), m_EntitiesToGhost.end(), entity) == m_EntitiesToGhost.end()) { m_EntitiesToGhost.push_back(entity); } - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) { CheckGhosting(entity); - return; } } @@ -413,14 +419,9 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr Game::server->Send(stream, sysAddr, false); } - if (entity->IsPlayer()) { - if (entity->GetGMLevel() > eGameMasterLevel::CIVILIAN) { - GameMessages::SendToggleGMInvis(entity->GetObjectID(), true, sysAddr); - } - } } -void EntityManager::ConstructAllEntities(const SystemAddress& sysAddr) { +void EntityManager::ConstructAllEntities(const SystemAddress& sysAddr) { //ZoneControl is special: ConstructEntity(m_ZoneControlEntity, sysAddr); @@ -488,11 +489,7 @@ void EntityManager::QueueGhostUpdate(LWOOBJID playerID) { void EntityManager::UpdateGhosting() { for (const auto playerID : m_PlayersToUpdateGhosting) { auto* player = PlayerManager::GetPlayer(playerID); - - if (player == nullptr) { - continue; - } - + if (!player) continue; UpdateGhosting(player); } @@ -519,6 +516,7 @@ void EntityManager::UpdateGhosting(Entity* player) { const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint); + auto ghostingDistanceMax = m_GhostDistanceMaxSquared; auto ghostingDistanceMin = m_GhostDistanceMinSqaured; @@ -555,35 +553,25 @@ void EntityManager::UpdateGhosting(Entity* player) { } void EntityManager::CheckGhosting(Entity* entity) { - if (entity == nullptr) { - return; - } + if (!entity) return; const auto& referencePoint = entity->GetPosition(); - for (auto* player : PlayerManager::GetAllPlayers()) { auto* ghostComponent = player->GetComponent(); if (!ghostComponent) continue; const auto& entityPoint = ghostComponent->GetGhostReferencePoint(); - const auto id = entity->GetObjectID(); - const auto observed = ghostComponent->IsObserved(id); - const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint); if (observed && distance > m_GhostDistanceMaxSquared) { ghostComponent->GhostEntity(id); - DestructEntity(entity, player->GetSystemAddress()); - entity->SetObservers(entity->GetObservers() - 1); } else if (!observed && m_GhostDistanceMinSqaured > distance) { ghostComponent->ObserveEntity(id); - ConstructEntity(entity, player->GetSystemAddress()); - entity->SetObservers(entity->GetObservers() + 1); } } diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index d86de72b..96283de3 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -1,4 +1,9 @@ #include "GhostComponent.h" +#include "PlayerManager.h" +#include "Character.h" +#include "ControllablePhysicsComponent.h" +#include "UserManager.h" +#include "User.h" #include "Amf3.h" #include "GameMessages.h" @@ -7,7 +12,9 @@ GhostComponent::GhostComponent(Entity* parent, const int32_t componentID) : Comp m_GhostReferencePoint = NiPoint3Constant::ZERO; m_GhostOverridePoint = NiPoint3Constant::ZERO; m_GhostOverride = false; - + + RegisterMsg(this, &GhostComponent::OnToggleGMInvis); + RegisterMsg(this, &GhostComponent::OnGetGMInvis); RegisterMsg(this, &GhostComponent::MsgGetObjectReportInfo); } @@ -22,6 +29,25 @@ GhostComponent::~GhostComponent() { } } +void GhostComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) { + auto* objElement = doc.FirstChildElement("obj"); + if (!objElement) return; + auto* ghstElement = objElement->FirstChildElement("ghst"); + if (!ghstElement) return; + m_IsGMInvisible = ghstElement->BoolAttribute("i"); +} + +void GhostComponent::UpdateXml(tinyxml2::XMLDocument& doc) { + auto* objElement = doc.FirstChildElement("obj"); + if (!objElement) return; + auto* ghstElement = objElement->FirstChildElement("ghst"); + if (ghstElement) objElement->DeleteChild(ghstElement); + // Only save if GM invisible + if (!m_IsGMInvisible) return; + ghstElement = objElement->InsertNewChildElement("ghst"); + if (ghstElement) ghstElement->SetAttribute("i", m_IsGMInvisible); +} + void GhostComponent::SetGhostReferencePoint(const NiPoint3& value) { m_GhostReferencePoint = value; } @@ -61,6 +87,40 @@ void GhostComponent::GhostEntity(LWOOBJID id) { m_ObservedEntities.erase(id); } +bool GhostComponent::OnToggleGMInvis(GameMessages::GameMsg& msg) { + auto& gmInvisMsg = static_cast(msg); + gmInvisMsg.bStateOut = !m_IsGMInvisible; + m_IsGMInvisible = !m_IsGMInvisible; + LOG_DEBUG("GM Invisibility toggled to: %s", m_IsGMInvisible ? "true" : "false"); + gmInvisMsg.Send(UNASSIGNED_SYSTEM_ADDRESS); + auto* thisUser = UserManager::Instance()->GetUser(m_Parent->GetSystemAddress()); + for (const auto& player : PlayerManager::GetAllPlayers()) { + if (!player || player->GetObjectID() == m_Parent->GetObjectID()) continue; + auto* toUser = UserManager::Instance()->GetUser(player->GetSystemAddress()); + if (m_IsGMInvisible) { + if (toUser->GetMaxGMLevel() < thisUser->GetMaxGMLevel()) { + Game::entityManager->DestructEntity(m_Parent, player->GetSystemAddress()); + } + } else { + if (toUser->GetMaxGMLevel() >= thisUser->GetMaxGMLevel()) { + Game::entityManager->ConstructEntity(m_Parent, player->GetSystemAddress()); + auto* controllableComp = m_Parent->GetComponent(); + controllableComp->SetDirtyPosition(true); + } + } + } + Game::entityManager->SerializeEntity(m_Parent); + + return true; +} + +bool GhostComponent::OnGetGMInvis(GameMessages::GameMsg& msg) { + LOG_DEBUG("GM Invisibility requested: %s", m_IsGMInvisible ? "true" : "false"); + auto& gmInvisMsg = static_cast(msg); + gmInvisMsg.bGMInvis = m_IsGMInvisible; + return gmInvisMsg.bGMInvis; +} + bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { auto& reportMsg = static_cast(msg); auto& cmptType = reportMsg.info->PushDebug("Ghost"); diff --git a/dGame/dComponents/GhostComponent.h b/dGame/dComponents/GhostComponent.h index 75ed3c9d..01443c1b 100644 --- a/dGame/dComponents/GhostComponent.h +++ b/dGame/dComponents/GhostComponent.h @@ -7,11 +7,17 @@ class NiPoint3; +namespace tinyxml2 { + class XMLDocument; +} + class GhostComponent final : public Component { public: static inline const eReplicaComponentType ComponentType = eReplicaComponentType::GHOST; GhostComponent(Entity* parent, const int32_t componentID); ~GhostComponent() override; + void LoadFromXml(const tinyxml2::XMLDocument& doc) override; + void UpdateXml(tinyxml2::XMLDocument& doc) override; void SetGhostOverride(bool value) { m_GhostOverride = value; }; @@ -39,9 +45,14 @@ public: void GhostEntity(const LWOOBJID id); + bool OnToggleGMInvis(GameMessages::GameMsg& msg); + + bool OnGetGMInvis(GameMessages::GameMsg& msg); + bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); private: + NiPoint3 m_GhostReferencePoint; NiPoint3 m_GhostOverridePoint; @@ -51,6 +62,9 @@ private: std::unordered_set m_LimboConstructions; bool m_GhostOverride; + + bool m_IsGMInvisible; + }; #endif //!__GHOSTCOMPONENT__H__ diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 522f4741..118baa5d 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -1847,18 +1847,6 @@ void GameMessages::SendNotifyClientFailedPrecondition(LWOOBJID objectId, const S SEND_PACKET; } -void GameMessages::SendToggleGMInvis(LWOOBJID objectId, bool enabled, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::TOGGLE_GM_INVIS); - bitStream.Write(enabled); // does not matter? - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - void GameMessages::SendSetName(LWOOBJID objectID, std::u16string name, const SystemAddress& sysAddr) { CBITSTREAM; CMSGHEADER; @@ -6449,4 +6437,8 @@ namespace GameMessages { stream.Write(lootID); stream.Write(lootOwnerID); } + + void ToggleGMInvis::Serialize(RakNet::BitStream& stream) const { + stream.Write(bStateOut); + } } diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 9daae4b5..3ae9ffce 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -243,8 +243,6 @@ namespace GameMessages { bool cancelOnLogout = false, bool cancelOnRemoveBuff = true, bool cancelOnUi = false, bool cancelOnUnequip = false, bool cancelOnZone = false, bool addedByTeammate = false, bool applyOnTeammates = false, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); - void SendToggleGMInvis(LWOOBJID objectId, bool enabled, const SystemAddress& sysAddr); - void SendSetName(LWOOBJID objectID, std::u16string name, const SystemAddress& sysAddr); // Property messages @@ -938,6 +936,20 @@ namespace GameMessages { LWOOBJID lootOwnerID{}; }; + struct ToggleGMInvis : public GameMsg { + ToggleGMInvis() : GameMsg(MessageType::Game::TOGGLE_GM_INVIS) {} + + void Serialize(RakNet::BitStream& stream) const override; + bool bStateOut{ false }; + + }; + + struct GetGMInvis : public GameMsg { + GetGMInvis() : GameMsg(MessageType::Game::GET_GM_INVIS) {} + + bool bGMInvis{ false }; + }; + struct ChildRemoved : public GameMsg { ChildRemoved() : GameMsg(MessageType::Game::CHILD_REMOVED) {} diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index 65ee97c6..6c2d96ee 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -893,10 +893,10 @@ void SlashCommandHandler::Startup() { Command GmInvisCommand{ .help = "Toggles invisibility for the character", - .info = "Toggles invisibility for the character, though it's currently a bit buggy. Requires nonzero GM Level for the character, but the account must have a GM level of 8", + .info = "Toggles invisibility for the character, making them invisible to other players and lower GM levels", .aliases = { "gminvis" }, .handle = GMGreaterThanZeroCommands::GmInvis, - .requiredLevel = eGameMasterLevel::DEVELOPER + .requiredLevel = eGameMasterLevel::FORUM_MODERATOR }; RegisterCommand(GmInvisCommand); diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 0fd86512..721ea828 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -89,7 +89,8 @@ namespace DEVGMCommands { GameMessages::SendChatModeUpdate(entity->GetObjectID(), eGameMasterLevel::CIVILIAN); entity->SetGMLevel(eGameMasterLevel::CIVILIAN); - GameMessages::SendToggleGMInvis(entity->GetObjectID(), false, UNASSIGNED_SYSTEM_ADDRESS); + GameMessages::ToggleGMInvis msg; + msg.Send(entity->GetObjectID()); GameMessages::SendSlashCommandFeedbackText(entity, u"Your game master level has been changed, you may not be able to use all commands."); } @@ -183,7 +184,6 @@ namespace DEVGMCommands { Game::entityManager->ConstructEntity(entity); ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(lowerName) + u" set to " + (GeneralUtils::to_u16string(minifigItemId))); - GameMessages::SendToggleGMInvis(entity->GetObjectID(), false, UNASSIGNED_SYSTEM_ADDRESS); // need to retoggle because it gets reenabled on creation of new character } void PlayAnimation(Entity* entity, const SystemAddress& sysAddr, const std::string args) { diff --git a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp index 7d017cf0..7575b9cc 100644 --- a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp @@ -275,7 +275,8 @@ namespace GMGreaterThanZeroCommands { } void GmInvis(Entity* entity, const SystemAddress& sysAddr, const std::string args) { - GameMessages::SendToggleGMInvis(entity->GetObjectID(), true, UNASSIGNED_SYSTEM_ADDRESS); + GameMessages::ToggleGMInvis msg; + msg.Send(entity->GetObjectID()); } void SetName(Entity* entity, const SystemAddress& sysAddr, const std::string args) { From 4658318a3a107e7e9297cdb7f61c158e5d580b1b Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:46:59 -0800 Subject: [PATCH 14/69] fix: deactivate bubble buff from server too (#1936) --- dGame/dGameMessages/GameMessages.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 118baa5d..03849d49 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -5931,6 +5931,7 @@ void GameMessages::HandleUpdatePlayerStatistic(RakNet::BitStream& inStream, Enti void GameMessages::HandleDeactivateBubbleBuff(RakNet::BitStream& inStream, Entity* entity) { auto controllablePhysicsComponent = entity->GetComponent(); if (controllablePhysicsComponent) controllablePhysicsComponent->DeactivateBubbleBuff(); + GameMessages::SendDeactivateBubbleBuffFromServer(entity->GetObjectID(), entity->GetSystemAddress()); } void GameMessages::HandleActivateBubbleBuff(RakNet::BitStream& inStream, Entity* entity) { From 86f8601bbd732c82e1bdcdf0db5e780d6aca8133 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:48:16 -0800 Subject: [PATCH 15/69] feat: various debug command improvements (#1934) * feat: various debug command improvements * add missing utility function * Update dGame/dUtilities/SlashCommands/DEVGMCommands.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../SlashCommands/DEVGMCommands.cpp | 104 ++++++++++++------ dNet/ChatPackets.cpp | 4 + dNet/ChatPackets.h | 1 + 3 files changed, 74 insertions(+), 35 deletions(-) diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 721ea828..ad4e0463 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -26,6 +26,8 @@ #include "Database.h" #include "CDObjectsTable.h" #include "CDRewardCodesTable.h" +#include "CDLootMatrixTable.h" +#include "CDLootTableTable.h" // Components #include "BuffComponent.h" @@ -177,11 +179,13 @@ namespace DEVGMCommands { charComp->m_Character->SetRightHand(minifigItemId); } else { Game::entityManager->ConstructEntity(entity); + Game::entityManager->ConstructEntity(entity, entity->GetSystemAddress()); ChatPackets::SendSystemMessage(sysAddr, u"Invalid Minifig item to change, try one of the following: Eyebrows, Eyes, HairColor, HairStyle, Pants, LeftHand, Mouth, RightHand, Shirt, Hands"); return; } Game::entityManager->ConstructEntity(entity); + Game::entityManager->ConstructEntity(entity, entity->GetSystemAddress()); ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(lowerName) + u" set to " + (GeneralUtils::to_u16string(minifigItemId))); } @@ -377,8 +381,6 @@ namespace DEVGMCommands { line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16(line), &entity, sysAddr); } - } else { - ChatPackets::SendSystemMessage(sysAddr, u"Unknown macro! Is the filename right?"); } } @@ -743,11 +745,40 @@ namespace DEVGMCommands { auto tables = query.execQuery(); + std::map lotToName{}; + std::map nameToLot{}; while (!tables.eof()) { - std::string message = std::to_string(tables.getIntField("id")) + " - " + tables.getStringField("name"); - ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16(message, message.size())); + const auto lot = tables.getIntField("id"); + const auto name = tables.getStringField("name"); + lotToName[lot] = name; + nameToLot[name] = lot; tables.nextRow(); } + + // if there arent a ton of results, print them to chat instead + if (lotToName.size() < 5) { + std::stringstream ss; + ss << "Lookup results for \"" << args << "\":"; + for (const auto& [lot, name] : lotToName) { + ss << "\nLOT: " << lot << " - Name: " << name; + } + ChatPackets::SendSystemMessage(sysAddr, ss.str()); + } else { + AMFArrayValue response; + response.Insert("visible", true); + response.Insert("objectID", "Search Results for: " + args); + response.Insert("serverInfo", true); + auto* const info = response.InsertArray("data"); + auto& lotSort = info->PushDebug("Sorted by LOT"); + for (const auto& [lot, name] : lotToName) { + auto& entry = lotSort.PushDebug(std::to_string(lot)) = name; + } + auto& nameSort = info->PushDebug("Sorted by Name"); + for (const auto& [name, lot] : nameToLot) { + auto& entry = nameSort.PushDebug(name) = std::to_string(lot); + } + GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, sysAddr); + } } void Spawn(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -1247,38 +1278,30 @@ namespace DEVGMCommands { } void Metrics(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + AMFArrayValue response; + response.Insert("visible", true); + response.Insert("objectID", "Metrics"); + response.Insert("serverInfo", true); + auto* info = response.InsertArray("data"); for (const auto variable : Metrics::GetAllMetrics()) { + auto& metricData = info->PushDebug(StringifiedEnum::ToString(variable)); + auto* metric = Metrics::GetMetric(variable); if (metric == nullptr) { continue; } - ChatPackets::SendSystemMessage( - sysAddr, - GeneralUtils::ASCIIToUTF16(Metrics::MetricVariableToString(variable)) + - u": " + - GeneralUtils::to_u16string(Metrics::ToMiliseconds(metric->average)) + - u"ms" - ); + metricData.PushDebug("Maximum") = std::to_string(Metrics::ToMiliseconds(metric->max)) + "ms"; + metricData.PushDebug("Minimum") = std::to_string(Metrics::ToMiliseconds(metric->min)) + "ms"; + metricData.PushDebug("Average") = std::to_string(Metrics::ToMiliseconds(metric->average)) + "ms"; + metricData.PushDebug("Measurements Count") = std::to_string(metric->measurementSize); } - - ChatPackets::SendSystemMessage( - sysAddr, - u"Peak RSS: " + GeneralUtils::to_u16string(static_cast(static_cast(Metrics::GetPeakRSS()) / 1.024e6)) + - u"MB" - ); - - ChatPackets::SendSystemMessage( - sysAddr, - u"Current RSS: " + GeneralUtils::to_u16string(static_cast(static_cast(Metrics::GetCurrentRSS()) / 1.024e6)) + - u"MB" - ); - - ChatPackets::SendSystemMessage( - sysAddr, - u"Process ID: " + GeneralUtils::to_u16string(Metrics::GetProcessID()) - ); + auto& processInfo = info->PushDebug("Process Info"); + processInfo.PushDebug("Peak RSS") = std::to_string(static_cast(Metrics::GetPeakRSS()) / 1.024e6) + "MB"; + processInfo.PushDebug("Current RSS") = std::to_string(static_cast(Metrics::GetCurrentRSS()) / 1.024e6) + "MB"; + processInfo.PushDebug("Process ID") = Metrics::GetProcessID(); + GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, sysAddr); } void ReloadConfig(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -1310,19 +1333,30 @@ namespace DEVGMCommands { const auto loops = GeneralUtils::TryParse(splitArgs[2]); if (!loops) return; + auto* const lootMatrixTable = CDClientManager::GetTable(); + auto* const lootTableTable = CDClientManager::GetTable(); + bool found = false; + for (const auto& entry : lootMatrixTable->GetMatrix(lootMatrixIndex.value())) { + for (const auto& loot : lootTableTable->GetTable(entry.LootTableIndex)) { + found = targetLot.value() == loot.itemid; + if (found) break; + } + } + + if (!found) { + std::stringstream ss; + ss << "Target LOT " << targetLot.value() << " not found in loot matrix " << lootMatrixIndex.value() << "."; + ChatPackets::SendSystemMessage(sysAddr, ss.str()); + return; + } + uint64_t totalRuns = 0; for (uint32_t i = 0; i < loops; i++) { while (true) { const auto lootRoll = Loot::RollLootMatrix(nullptr, lootMatrixIndex.value()); totalRuns += 1; - bool doBreak = false; - for (const auto& kv : lootRoll) { - if (static_cast(kv.first) == targetLot) { - doBreak = true; - } - } - if (doBreak) break; + if (lootRoll.contains(targetLot.value())) break; } } diff --git a/dNet/ChatPackets.cpp b/dNet/ChatPackets.cpp index 86622ad1..0c54e214 100644 --- a/dNet/ChatPackets.cpp +++ b/dNet/ChatPackets.cpp @@ -58,6 +58,10 @@ void ChatPackets::SendChatMessage(const SystemAddress& sysAddr, char chatChannel SEND_PACKET_BROADCAST; } +void ChatPackets::SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, const bool broadcast) { + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(message), broadcast); +} + void ChatPackets::SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, const bool broadcast) { CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE); diff --git a/dNet/ChatPackets.h b/dNet/ChatPackets.h index 53d0eced..8139fb03 100644 --- a/dNet/ChatPackets.h +++ b/dNet/ChatPackets.h @@ -58,6 +58,7 @@ namespace ChatPackets { }; void SendChatMessage(const SystemAddress& sysAddr, char chatChannel, const std::string& senderName, LWOOBJID playerObjectID, bool senderMythran, const std::u16string& message); + void SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, bool broadcast = false); void SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, bool broadcast = false); void SendMessageFail(const SystemAddress& sysAddr); void SendRoutedMsg(const LUBitStream& msg, const LWOOBJID targetID, const SystemAddress& sysAddr); From 5410acffaa53bdca7960377ea1722f62413c382c Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:48:57 -0800 Subject: [PATCH 16/69] fix: chat server crash (#1931) * fix: chat server crash * Update dChatServer/ChatServer.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dChatServer/ChatServer.cpp | 3 +++ dWeb/Web.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index 34b4d6e3..13f2bde0 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -202,8 +202,11 @@ int main(int argc, char** argv) { //Delete our objects here: Database::Destroy("ChatServer"); delete Game::server; + Game::server = nullptr; delete Game::logger; + Game::logger = nullptr; delete Game::config; + Game::config = nullptr; return EXIT_SUCCESS; } diff --git a/dWeb/Web.cpp b/dWeb/Web.cpp index abf9bd36..f8cf2edf 100644 --- a/dWeb/Web.cpp +++ b/dWeb/Web.cpp @@ -184,7 +184,7 @@ static void DLOG(char ch, void *param) { static size_t len{}; if (ch != '\n') buf[len++] = ch; // we provide the newline in our logger if (ch == '\n' || len >= sizeof(buf)) { - LOG_DEBUG("%.*s", static_cast(len), buf); + if (Game::logger) LOG_DEBUG("%.*s", static_cast(len), buf); len = 0; } } From 991e55f305f55e934585c73316ade448f1438aa0 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 16 Nov 2025 14:17:26 -0800 Subject: [PATCH 17/69] feat: dont drop loot for dead players if configured in the zone activity settings (#1935) * feat: dont drop loot for dead players if configured in the zone activity settings * fix errors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dGame/dComponents/ActivityComponent.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dGame/dUtilities/Loot.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../CDClientTables/CDActivitiesTable.cpp | 11 ++- .../CDClientTables/CDActivitiesTable.h | 2 + dGame/dComponents/ActivityComponent.h | 4 + dGame/dComponents/DestroyableComponent.cpp | 7 ++ dGame/dComponents/DestroyableComponent.h | 1 + dGame/dGameMessages/GameMessages.h | 6 ++ dGame/dUtilities/Loot.cpp | 90 ++++++++++++------- 7 files changed, 90 insertions(+), 31 deletions(-) diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp index 19111490..f84e692b 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp @@ -1,6 +1,5 @@ #include "CDActivitiesTable.h" - void CDActivitiesTable::LoadValuesFromDatabase() { // First, get the size of the table uint32_t size = 0; @@ -56,3 +55,13 @@ std::vector CDActivitiesTable::Query(std::function CDActivitiesTable::GetActivity(const uint32_t activityID) { + auto& entries = GetEntries(); + for (const auto& entry : entries) { + if (entry.ActivityID == activityID) { + return entry; + } + } + return std::nullopt; +} diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h index 3e1d4c37..86731bba 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h @@ -2,6 +2,7 @@ // Custom Classes #include "CDTable.h" +#include struct CDActivities { uint32_t ActivityID; @@ -31,4 +32,5 @@ public: // Queries the table with a custom "where" clause std::vector Query(std::function predicate); + std::optional GetActivity(const uint32_t activityID); }; diff --git a/dGame/dComponents/ActivityComponent.h b/dGame/dComponents/ActivityComponent.h index dc249068..bcaa5bfc 100644 --- a/dGame/dComponents/ActivityComponent.h +++ b/dGame/dComponents/ActivityComponent.h @@ -215,6 +215,10 @@ public: */ int GetActivityID() { return m_ActivityInfo.ActivityID; } + // Whether or not team loot should be dropped on death for this activity + // if true, and a player is supposed to get loot, they are skipped + bool GetNoTeamLootOnDeath() const { return m_ActivityInfo.noTeamLootOnDeath; } + /** * Returns if this activity has a lobby, e.g. if it needs to instance players to some other map * @return true if this activity has a lobby, false otherwise diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 47aa0c90..86969c5a 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -88,6 +88,7 @@ DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t compone RegisterMsg(this, &DestroyableComponent::OnGetObjectReportInfo); RegisterMsg(this, &DestroyableComponent::OnSetFaction); + RegisterMsg(this, &DestroyableComponent::OnIsDead); } DestroyableComponent::~DestroyableComponent() { @@ -1191,3 +1192,9 @@ bool DestroyableComponent::OnSetFaction(GameMessages::GameMsg& msg) { SetFaction(modifyFaction.factionID, modifyFaction.bIgnoreChecks); return true; } + +bool DestroyableComponent::OnIsDead(GameMessages::GameMsg& msg) { + auto& isDeadMsg = static_cast(msg); + isDeadMsg.bDead = m_IsDead || (GetHealth() == 0 && GetArmor() == 0); + return true; +} diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 9b3e46af..0b4d2ba8 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -472,6 +472,7 @@ public: bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); bool OnSetFaction(GameMessages::GameMsg& msg); + bool OnIsDead(GameMessages::GameMsg& msg); void SetIsDead(const bool value) { m_IsDead = value; } diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 3ae9ffce..3b27b9f1 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -955,5 +955,11 @@ namespace GameMessages { LWOOBJID childID{}; }; + + struct IsDead : public GameMsg { + IsDead() : GameMsg(MessageType::Game::IS_DEAD) {} + + bool bDead{}; + }; }; #endif // GAMEMESSAGES_H diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index ae6f6177..2fe7b796 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -21,6 +21,8 @@ #include "TeamManager.h" #include "CDObjectsTable.h" #include "ObjectIDManager.h" +#include "CDActivitiesTable.h" +#include "ScriptedActivityComponent.h" namespace { std::unordered_set CachedMatrices; @@ -142,13 +144,17 @@ void DropFactionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Drops 1 token for each player on a team // token drops are always given to every player on the team. -void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { for (const auto member : team.members) { GameMessages::GetPosition memberPosMsg{}; memberPosMsg.target = member; memberPosMsg.Send(); if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue; + GameMessages::IsDead isDeadMsg{}; + // Skip dead players + if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue; + GameMessages::GetFactionTokenType factionTokenType{}; factionTokenType.target = member; // If we're not in a faction, this message will return false @@ -186,7 +192,7 @@ void DropPowerupLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Drop the power up with no owner // Power ups can be picked up by anyone on a team, however unlike actual loot items, // if multiple clients say they picked one up, we let them pick it up. -void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { lootMsg.lootID = ObjectIDManager::GenerateObjectID(); lootMsg.ownerID = LWOOBJID_EMPTY; // By setting ownerID to empty, any client that gets this DropClientLoot message can pick up the item. CalcFinalDropPos(lootMsg); @@ -198,6 +204,10 @@ void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { memberPosMsg.Send(); if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue; + GameMessages::IsDead isDeadMsg{}; + // Skip dead players + if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue; + lootMsg.target = member; // By sending this message with the same ID to all players on the team, all players on the team are allowed to pick it up. lootMsg.Send(); @@ -230,7 +240,7 @@ void DropMissionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Check if the item needs to be dropped for anyone on the team // Only players who need the item will have it dropped -void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { GameMessages::MissionNeedsLot needMsg{}; needMsg.item = lootMsg.item; for (const auto member : team.members) { @@ -239,6 +249,10 @@ void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { memberPosMsg.Send(); if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue; + GameMessages::IsDead isDeadMsg{}; + // Skip dead players + if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue; + needMsg.target = member; // Will return false if the item is not required if (needMsg.Send()) { @@ -274,19 +288,24 @@ void DropRegularLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Drop a regular piece of loot. // Most items will go through this. // Finds the next loot owner on the team the is in range of the kill and gives them this reward. -void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { auto earningPlayer = LWOOBJID_EMPTY; lootMsg.lootID = ObjectIDManager::GenerateObjectID(); CalcFinalDropPos(lootMsg); GameMessages::GetPosition memberPosMsg{}; + GameMessages::IsDead isDeadMsg{}; // Find the next loot owner. Eventually this will run into the `player` passed into this function, since those will // have the same ID, this loop will only ever run at most 4 times. do { earningPlayer = team.GetNextLootOwner(); memberPosMsg.target = earningPlayer; memberPosMsg.Send(); - } while (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS); - + if (noTeamLootOnDeath) { + isDeadMsg.target = earningPlayer; + // Skip dead players + isDeadMsg.Send(); + } + } while (isDeadMsg.bDead || (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS)); if (team.lootOption == 0 /* Shared loot */) { lootMsg.target = earningPlayer; lootMsg.ownerID = earningPlayer; @@ -304,7 +323,7 @@ void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg) { DistrbuteMsgToTeam(lootMsg, team); } -void DropLoot(Entity* player, const LWOOBJID source, const std::map& rolledItems, uint32_t minCoins, uint32_t maxCoins) { +void DropLoot(Entity* player, const LWOOBJID source, const std::map& rolledItems, uint32_t minCoins, uint32_t maxCoins, const bool noTeamLootOnDeath) { player = player->GetOwner(); // if the owner is overwritten, we collect that here const auto playerID = player->GetObjectID(); if (!player || !player->IsPlayer()) { @@ -342,45 +361,53 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::mapGetByID(lootLot); if (lootLot == TOKEN_PROXY) { - team ? DropFactionLoot(*team, lootMsg) : DropFactionLoot(*player, lootMsg); + team ? DropFactionLoot(*team, lootMsg, noTeamLootOnDeath) : DropFactionLoot(*player, lootMsg); } else if (info.table.MissionDrop) { - team ? DropMissionLoot(*team, lootMsg) : DropMissionLoot(*player, lootMsg); + team ? DropMissionLoot(*team, lootMsg, noTeamLootOnDeath) : DropMissionLoot(*player, lootMsg); } else if (object.type == "Powerup") { - team ? DropPowerupLoot(*team, lootMsg) : DropPowerupLoot(*player, lootMsg); + team ? DropPowerupLoot(*team, lootMsg, noTeamLootOnDeath) : DropPowerupLoot(*player, lootMsg); } else { - team ? DropRegularLoot(*team, lootMsg) : DropRegularLoot(*player, lootMsg); + team ? DropRegularLoot(*team, lootMsg, noTeamLootOnDeath) : DropRegularLoot(*player, lootMsg); } } } - // Coin roll is divided up between the members, rounded up, then dropped for each player - const uint32_t coinRoll = static_cast(minCoins + GeneralUtils::GenerateRandomNumber(0, 1) * (maxCoins - minCoins)); - const auto droppedCoins = team ? std::ceil(static_cast(coinRoll) / team->members.size()) : coinRoll; + // Filter out dead player if we need to + std::vector lootEarners; if (team) { - for (auto member : team->members) { - GameMessages::DropClientLoot lootMsg{}; - lootMsg.target = member; - lootMsg.ownerID = member; - lootMsg.currency = droppedCoins; - lootMsg.spawnPos = spawnPosition; - lootMsg.sourceID = source; - lootMsg.item = LOT_NULL; - CalcFinalDropPos(lootMsg); - lootMsg.Send(); - const auto* const memberEntity = Game::entityManager->GetEntity(member); - if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress()); + if (noTeamLootOnDeath) { + for (const auto member : team->members) { + GameMessages::IsDead isDeadMsg{}; + isDeadMsg.target = member; + if (isDeadMsg.Send() && !isDeadMsg.bDead) { + lootEarners.push_back(member); + } + } + } else { + lootEarners = team->members; } } else { + lootEarners.push_back(playerID); + } + + // Coin roll is divided up between the members, rounded up, then dropped for each player + const uint32_t coinRoll = static_cast(minCoins + GeneralUtils::GenerateRandomNumber(0, 1) * (maxCoins - minCoins)); + // Just in case its empty don't allow divide by 0 + const auto droppedCoins = lootEarners.empty() ? coinRoll : static_cast(std::ceil(static_cast(coinRoll) / lootEarners.size())); + + // Drops coins for each alive member of a team (or just a player) + for (auto member : lootEarners) { GameMessages::DropClientLoot lootMsg{}; - lootMsg.target = playerID; - lootMsg.ownerID = playerID; + lootMsg.target = member; + lootMsg.ownerID = member; lootMsg.currency = droppedCoins; lootMsg.spawnPos = spawnPosition; lootMsg.sourceID = source; lootMsg.item = LOT_NULL; CalcFinalDropPos(lootMsg); lootMsg.Send(); - lootMsg.Send(player->GetSystemAddress()); + const auto* const memberEntity = Game::entityManager->GetEntity(member); + if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress()); } } @@ -533,6 +560,9 @@ void Loot::GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t acti void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins) { player = player->GetOwner(); // if the owner is overwritten, we collect that here + auto* scriptedActivityComponent = Game::entityManager->GetZoneControlEntity()->GetComponent(); + const bool noTeamLootOnDeath = scriptedActivityComponent ? scriptedActivityComponent->GetNoTeamLootOnDeath() : false; + auto* inventoryComponent = player->GetComponent(); if (!inventoryComponent) @@ -540,7 +570,7 @@ void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, const auto result = ::RollLootMatrix(matrixIndex); - ::DropLoot(player, source, result, minCoins, maxCoins); + ::DropLoot(player, source, result, minCoins, maxCoins, noTeamLootOnDeath); } void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) { From ca7424cbeb6ed23a44991ad5e8ca9c229cebdc11 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 16 Nov 2025 14:17:49 -0800 Subject: [PATCH 18/69] fix: chest loot not working (#1933) * fix bons and dragon loot * fix chest server loot --- .../General/TreasureChestDragonServer.cpp | 20 +++------------- .../General/MinigameTreasureChestServer.cpp | 23 ++++--------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp b/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp index ec6327e0..b0a98afc 100644 --- a/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp +++ b/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp @@ -20,24 +20,10 @@ void TreasureChestDragonServer::OnUse(Entity* self, Entity* user) { if (scriptedActivityComponent == nullptr) { return; } - - auto rating = 1; - + auto* team = TeamManager::Instance()->GetTeam(user->GetObjectID()); - - if (team != nullptr) { - rating = team->members.size(); - - for (const auto member : team->members) { - auto* memberObject = Game::entityManager->GetEntity(member); - - if (memberObject == nullptr) continue; - - Loot::DropActivityLoot(memberObject, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), rating); - } - } else { - Loot::DropActivityLoot(user, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), rating); - } + + Loot::DropActivityLoot(user, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), team ? team->members.size() : 1); self->Smash(self->GetObjectID()); } diff --git a/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp b/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp index 7b0abaa1..00cc68d6 100644 --- a/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp +++ b/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp @@ -4,6 +4,7 @@ #include "EntityManager.h" #include "dZoneManager.h" #include "Loot.h" +#include "dServer.h" void MinigameTreasureChestServer::OnUse(Entity* self, Entity* user) { auto* sac = self->GetComponent(); @@ -18,25 +19,9 @@ void MinigameTreasureChestServer::OnUse(Entity* self, Entity* user) { UpdatePlayer(self, user->GetObjectID()); auto* team = TeamManager::Instance()->GetTeam(user->GetObjectID()); - uint32_t activityRating = 0; - if (team != nullptr) { - for (const auto& teamMemberID : team->members) { - auto* teamMember = Game::entityManager->GetEntity(teamMemberID); - if (teamMember != nullptr) { - activityRating = CalculateActivityRating(self, teamMemberID); - - if (self->GetLOT() == frakjawChestId) activityRating = team->members.size(); - - Loot::DropActivityLoot(teamMember, self->GetObjectID(), sac->GetActivityID(), activityRating); - } - } - } else { - activityRating = CalculateActivityRating(self, user->GetObjectID()); - - if (self->GetLOT() == frakjawChestId) activityRating = 1; - - Loot::DropActivityLoot(user, self->GetObjectID(), sac->GetActivityID(), activityRating); - } + uint32_t activityRating = CalculateActivityRating(self, user->GetObjectID()); + if (self->GetLOT() == frakjawChestId || Game::server->GetZoneID() == 1204) activityRating = team != nullptr ? team->members.size() : 1; + Loot::DropActivityLoot(user, self->GetObjectID(), sac->GetActivityID(), activityRating); sac->PlayerRemove(user->GetObjectID()); From ea86a708e469d79354220ffe2355f3dca02f9b49 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 18 Nov 2025 19:06:03 -0800 Subject: [PATCH 19/69] fix: uninitialized memory (#1937) --- dGame/dComponents/GhostComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dGame/dComponents/GhostComponent.h b/dGame/dComponents/GhostComponent.h index 01443c1b..8ea05397 100644 --- a/dGame/dComponents/GhostComponent.h +++ b/dGame/dComponents/GhostComponent.h @@ -63,7 +63,7 @@ private: bool m_GhostOverride; - bool m_IsGMInvisible; + bool m_IsGMInvisible{ false }; }; From a713216540f833b6e2a2dbeccb8aa40da1e9d86a Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:04:07 -0800 Subject: [PATCH 20/69] fix: saving gm invis for non gms (#1940) --- dGame/User.h | 2 +- dGame/dComponents/GhostComponent.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dGame/User.h b/dGame/User.h index 7fe8d335..2c9f374c 100644 --- a/dGame/User.h +++ b/dGame/User.h @@ -31,7 +31,7 @@ public: std::string& GetSessionKey() { return m_SessionKey; } SystemAddress& GetSystemAddress() { return m_SystemAddress; } - eGameMasterLevel GetMaxGMLevel() { return m_MaxGMLevel; } + eGameMasterLevel GetMaxGMLevel() const { return m_MaxGMLevel; } uint32_t GetLastCharID() { return m_LastCharID; } void SetLastCharID(uint32_t newCharID) { m_LastCharID = newCharID; } diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index 96283de3..a354e2c6 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -12,7 +12,7 @@ GhostComponent::GhostComponent(Entity* parent, const int32_t componentID) : Comp m_GhostReferencePoint = NiPoint3Constant::ZERO; m_GhostOverridePoint = NiPoint3Constant::ZERO; m_GhostOverride = false; - + RegisterMsg(this, &GhostComponent::OnToggleGMInvis); RegisterMsg(this, &GhostComponent::OnGetGMInvis); RegisterMsg(this, &GhostComponent::MsgGetObjectReportInfo); @@ -43,7 +43,8 @@ void GhostComponent::UpdateXml(tinyxml2::XMLDocument& doc) { auto* ghstElement = objElement->FirstChildElement("ghst"); if (ghstElement) objElement->DeleteChild(ghstElement); // Only save if GM invisible - if (!m_IsGMInvisible) return; + const auto* const user = UserManager::Instance()->GetUser(m_Parent->GetSystemAddress()); + if (!m_IsGMInvisible || !user || user->GetMaxGMLevel() < eGameMasterLevel::FORUM_MODERATOR) return; ghstElement = objElement->InsertNewChildElement("ghst"); if (ghstElement) ghstElement->SetAttribute("i", m_IsGMInvisible); } From bf020baa17543cea62e26b319bf1286afa815451 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:39:33 -0800 Subject: [PATCH 21/69] fix: temp fixes for ghosting so I can continue being on break (#1947) * fix: temp fixes for ghosting so I can continue being on break disables the ghost feature for now so i can continue my break * Update GhostComponent.cpp --- dGame/dComponents/GhostComponent.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index a354e2c6..d5f3b9bc 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -89,6 +89,8 @@ void GhostComponent::GhostEntity(LWOOBJID id) { } bool GhostComponent::OnToggleGMInvis(GameMessages::GameMsg& msg) { + // TODO: disabled for now while bugs are fixed + return false; auto& gmInvisMsg = static_cast(msg); gmInvisMsg.bStateOut = !m_IsGMInvisible; m_IsGMInvisible = !m_IsGMInvisible; @@ -118,8 +120,11 @@ bool GhostComponent::OnToggleGMInvis(GameMessages::GameMsg& msg) { bool GhostComponent::OnGetGMInvis(GameMessages::GameMsg& msg) { LOG_DEBUG("GM Invisibility requested: %s", m_IsGMInvisible ? "true" : "false"); auto& gmInvisMsg = static_cast(msg); - gmInvisMsg.bGMInvis = m_IsGMInvisible; - return gmInvisMsg.bGMInvis; + // TODO: disabled for now while bugs are fixed + // gmInvisMsg.bGMInvis = m_IsGMInvisible; + // return gmInvisMsg.bGMInvis; + gmInvisMsg.bGMInvis = false; + return false; } bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { From 40fef36530c256871a4f59f106cfae28fe824e70 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:00:58 -0800 Subject: [PATCH 22/69] fix: coins dropping on killer (#1948) tested that when a player dies the coins spawn on their body instead --- dGame/dComponents/DestroyableComponent.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 86969c5a..0f886d84 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -755,7 +755,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType const auto isPlayer = m_Parent->IsPlayer(); - GameMessages::SendDie(m_Parent, source, source, true, killType, deathType, 0, 0, 0, isPlayer, false, 1); + GameMessages::SendDie(m_Parent, source, source, true, killType, deathType, 0, 0, 0, isPlayer, true, 1); //NANI?! if (!isPlayer) { @@ -785,8 +785,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType lootMsg.sourceID = source; lootMsg.item = LOT_NULL; lootMsg.Send(); - lootMsg.Send(m_Parent->GetSystemAddress()); - character->SetCoins(coinsTotal, eLootSourceType::PICKUP); + character->SetCoins(coinsTotal, eLootSourceType::DELETION); } } From 66b7d3606e3e11b595de56373018fb01082cbde7 Mon Sep 17 00:00:00 2001 From: Terrev <21133460+Terrev@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:58:10 -0500 Subject: [PATCH 23/69] fix: flower activity (#1950) --- dScripts/02_server/Map/General/GrowingFlower.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dScripts/02_server/Map/General/GrowingFlower.cpp b/dScripts/02_server/Map/General/GrowingFlower.cpp index 04cc11ab..2c3929e2 100644 --- a/dScripts/02_server/Map/General/GrowingFlower.cpp +++ b/dScripts/02_server/Map/General/GrowingFlower.cpp @@ -2,6 +2,7 @@ #include "MissionComponent.h" #include "eMissionTaskType.h" #include "eMissionState.h" +#include "ScriptedActivityComponent.h" #include "Loot.h" void GrowingFlower::OnSkillEventFired(Entity* self, Entity* target, const std::string& message) { @@ -13,7 +14,10 @@ void GrowingFlower::OnSkillEventFired(Entity* self, Entity* target, const std::s const auto mission1 = self->GetVar(u"missionID"); const auto mission2 = self->GetVar(u"missionID2"); - Loot::DropActivityLoot(target, self->GetObjectID(), self->GetLOT(), 0); + auto* scriptedActivityComponent = self->GetComponent(); + if (scriptedActivityComponent != nullptr) { + Loot::DropActivityLoot(target, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), 0); + } auto* missionComponent = target->GetComponent(); if (missionComponent != nullptr) { From c723ce25882d5af01635ed957b87d523b26c4feb Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:48:29 -0800 Subject: [PATCH 24/69] fix: donations requiring new high score vs adding to previous one (#1951) tested that jawbox works as intended now for donation counting on the leaderboards --- dGame/LeaderboardManager.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp index 488bc2b0..a6f13c65 100644 --- a/dGame/LeaderboardManager.cpp +++ b/dGame/LeaderboardManager.cpp @@ -289,6 +289,10 @@ void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activi ILeaderboard::Score oldScoreFlipped{oldScore->secondaryScore, oldScore->primaryScore, oldScore->tertiaryScore}; ILeaderboard::Score newScoreFlipped{newScore.secondaryScore, newScore.primaryScore, newScore.tertiaryScore}; newHighScore = newScoreFlipped > oldScoreFlipped; + } else if (leaderboardType == Leaderboard::Type::Donations) { + // Donations just need to go up if updated + newHighScore = true; + newScore.primaryScore += oldScore->primaryScore; } if (newHighScore) { From 347b1d17d42596c2085e8390f5dac5c6246d2123 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:49:39 -0800 Subject: [PATCH 25/69] fix: not checking pending names on rename (#1954) --- dGame/UserManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dGame/UserManager.cpp b/dGame/UserManager.cpp index 15fbc2d6..87bbfc90 100644 --- a/dGame/UserManager.cpp +++ b/dGame/UserManager.cpp @@ -514,7 +514,7 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) return; } - if (!Database::Get()->GetCharacterInfo(newName)) { + if (!Database::Get()->IsNameInUse(newName)) { if (autoRejectNames) { Database::Get()->SetCharacterName(objectID, newName); LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str()); From 3c64b26c39519d9c4f71970824f53525c6e30828 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:49:51 -0800 Subject: [PATCH 26/69] fix: macos ci (#1955) --- .github/workflows/build-and-test.yml | 2 +- CMakePresets.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c3eae601..f0806156 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -13,7 +13,7 @@ jobs: continue-on-error: true strategy: matrix: - os: [ windows-2022, ubuntu-22.04, macos-13 ] + os: [ windows-2022, ubuntu-22.04, macos-15-intel ] steps: - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 diff --git a/CMakePresets.json b/CMakePresets.json index 3ed904e7..9806a3a3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -616,7 +616,7 @@ ] }, { - "name": "ci-macos-13", + "name": "ci-macos-15-intel", "displayName": "[Release] MacOS", "description": "CI workflow preset for MacOS", "steps": [ From 434c9b6315ccccd04acd8edb0a9d527c232a847b Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:16:36 -0800 Subject: [PATCH 27/69] fix: imaginite on racing minigames and add null checks (#1958) --- dGame/dComponents/ActivityComponent.cpp | 3 +-- dGame/dComponents/RacingControlComponent.cpp | 4 ++-- dGame/dComponents/RacingControlComponent.h | 4 ++-- dScripts/ActivityManager.cpp | 3 +++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/dGame/dComponents/ActivityComponent.cpp b/dGame/dComponents/ActivityComponent.cpp index 7567e20b..c5436df8 100644 --- a/dGame/dComponents/ActivityComponent.cpp +++ b/dGame/dComponents/ActivityComponent.cpp @@ -325,9 +325,8 @@ bool ActivityComponent::CheckCost(Entity* player) const { } bool ActivityComponent::TakeCost(Entity* player) const { - auto* inventoryComponent = player->GetComponent(); - return CheckCost(player) && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL); + return CheckCost(player) && inventoryComponent && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL); } void ActivityComponent::PlayerReady(Entity* player, bool bReady) { diff --git a/dGame/dComponents/RacingControlComponent.cpp b/dGame/dComponents/RacingControlComponent.cpp index 8749d64e..6d0f675b 100644 --- a/dGame/dComponents/RacingControlComponent.cpp +++ b/dGame/dComponents/RacingControlComponent.cpp @@ -33,7 +33,7 @@ #endif RacingControlComponent::RacingControlComponent(Entity* parent, const int32_t componentID) - : Component(parent, componentID) { + : ActivityComponent(parent, componentID) { m_PathName = u"MainPath"; m_NumberOfLaps = 3; m_RemainingLaps = m_NumberOfLaps; @@ -70,7 +70,7 @@ void RacingControlComponent::OnPlayerLoaded(Entity* player) { auto* vehicle = inventoryComponent->FindItemByLot(8092); // If the race has already started, send the player back to the main world. - if (m_Loaded || !vehicle) { + if (m_Loaded || !vehicle || !TakeCost(player)) { auto* characterComponent = player->GetComponent(); if (characterComponent) characterComponent->SendToZone(m_MainWorld); return; diff --git a/dGame/dComponents/RacingControlComponent.h b/dGame/dComponents/RacingControlComponent.h index a1e99e29..b4300d94 100644 --- a/dGame/dComponents/RacingControlComponent.h +++ b/dGame/dComponents/RacingControlComponent.h @@ -6,7 +6,7 @@ #include "BitStream.h" #include "Entity.h" -#include "Component.h" +#include "ActivityComponent.h" #include "eReplicaComponentType.h" #include @@ -104,7 +104,7 @@ struct RacingPlayerInfo { /** * Component that's attached to a manager entity in each race zone that loads player vehicles, keep scores, etc. */ -class RacingControlComponent final : public Component { +class RacingControlComponent final : public ActivityComponent { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::RACING_CONTROL; diff --git a/dScripts/ActivityManager.cpp b/dScripts/ActivityManager.cpp index 060fda08..047eb76f 100644 --- a/dScripts/ActivityManager.cpp +++ b/dScripts/ActivityManager.cpp @@ -7,6 +7,7 @@ #include "Logger.h" #include "Loot.h" #include "ShootingGalleryComponent.h" +#include "RacingControlComponent.h" bool ActivityManager::IsPlayerInActivity(Entity* self, LWOOBJID playerID) { const auto* sac = self->GetComponent(); @@ -99,6 +100,8 @@ bool ActivityManager::TakeActivityCost(const Entity* self, const LWOOBJID player activityComponent = self->GetComponent(); } + if (!activityComponent) return false; + auto* player = Game::entityManager->GetEntity(playerID); if (player == nullptr) return false; From 8283d1fa95acf4fb90b3ca5ca1f119e5db603527 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 29 Mar 2026 13:59:09 -0500 Subject: [PATCH 28/69] fix: mariadb on newer gcc and newer cmake version (#1961) * Fix newer gcc issues in mariadb * fix error with newer cmake versions * update mariadb to latest * fix macos and docker * fix: update Windows MSI package comments and align Connector/C++ version * Update cmake/FindMariaDB.cmake Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: only pass CMAKE_POLICY_VERSION_MINIMUM to ExternalProject when CMake >= 4.0 Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/a247f729-a0b1-4fb6-825e-d23045b1ee55 Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- CMakeLists.txt | 11 ++++++++- Dockerfile | 4 ++- cmake/FindMariaDB.cmake | 42 +++++++++++++++++++++++--------- thirdparty/mariadb-connector-cpp | 2 +- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f9802f1d..76930398 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,11 @@ set(CMAKE_C_STANDARD 99) set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0") + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) +endif() + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debugging set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions @@ -67,7 +72,11 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE) # Disabled no-register # Disabled unknown pragmas because Linux doesn't understand Windows pragmas. if(UNIX) - add_link_options("-Wl,-rpath,$ORIGIN/") + if(APPLE) + add_link_options("-Wl,-rpath,@loader_path/") + else() + add_link_options("-Wl,-rpath,$ORIGIN/") + endif() add_compile_options("-fPIC") add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0) diff --git a/Dockerfile b/Dockerfile index 8c722f7e..da99f066 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,9 @@ RUN --mount=type=cache,target=/app/build,id=build-cache \ cd /app/build && \ cmake .. && \ make -j$(nproc --ignore 1) && \ - cp -r /app/build/* /tmp/persisted-build/ + cp -r /app/build/* /tmp/persisted-build/ && \ + mkdir -p /tmp/persisted-build/mariadbcpp && \ + cp /app/build/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build/libmariadbcpp.so /tmp/persisted-build/mariadbcpp/ FROM debian:12 as runtime diff --git a/cmake/FindMariaDB.cmake b/cmake/FindMariaDB.cmake index d3f89931..c76d2d3a 100644 --- a/cmake/FindMariaDB.cmake +++ b/cmake/FindMariaDB.cmake @@ -10,14 +10,15 @@ if(WIN32 AND NOT MARIADB_BUILD_SOURCE) file(MAKE_DIRECTORY "${MARIADB_MSI_DIR}") file(MAKE_DIRECTORY "${MARIADB_CONNECTOR_DIR}") - # These values need to be updated whenever a new minor release replaces an old one - # Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts - set(MARIADB_CONNECTOR_C_VERSION "3.2.7") - set(MARIADB_CONNECTOR_C_BUCKET "2319651") - set(MARIADB_CONNECTOR_C_MD5 "f8636d733f1d093af9d4f22f3239f885") - set(MARIADB_CONNECTOR_CPP_VERSION "1.0.2") - set(MARIADB_CONNECTOR_CPP_BUCKET "2531525") - set(MARIADB_CONNECTOR_CPP_MD5 "3034bbd6ca00a0125345f9fd1a178401") + # These values track the published Windows MSI packages used by the prebuilt path. + # Keep the Connector/C++ package version aligned with the checked out submodule tag when possible. + # Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts. + set(MARIADB_CONNECTOR_C_VERSION "3.4.8") + set(MARIADB_CONNECTOR_C_BUCKET "4516894") + set(MARIADB_CONNECTOR_C_MD5 "50f6fc0c77b8d3bacbeac0126e179861") + set(MARIADB_CONNECTOR_CPP_VERSION "1.1.7") + set(MARIADB_CONNECTOR_CPP_BUCKET "4464908") + set(MARIADB_CONNECTOR_CPP_MD5 "08644a7ff084b5933325cadb904796e5") set(MARIADB_CONNECTOR_C_MSI "mariadb-connector-c-${MARIADB_CONNECTOR_C_VERSION}-win64.msi") set(MARIADB_CONNECTOR_CPP_MSI "mariadb-connector-cpp-${MARIADB_CONNECTOR_CPP_VERSION}-win64.msi") @@ -79,23 +80,39 @@ else() # Build from source -DWITH_EXTERNAL_ZLIB=ON -DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR} -DCMAKE_C_FLAGS=-w # disable zlib warnings - -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0) + -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -include\ cstdint) else() set(MARIADB_EXTRA_CMAKE_ARGS -DCMAKE_C_FLAGS=-w # disable zlib warnings - -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0) + -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -include\ cstdint) endif() + set(MARIADBCPP_BUILD_DIR "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build") set(MARIADBCPP_INSTALL_DIR ${PROJECT_BINARY_DIR}/prefix) - set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp) - set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin) set(MARIADBCPP_SOURCE_DIR ${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp) set(MARIADB_INCLUDE_DIR "${MARIADBCPP_SOURCE_DIR}/include") + + if(WIN32) + set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp) + set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin) + set(MARIADB_INSTALL_COMMAND) + else() + set(MARIADBCPP_LIBRARY_DIR ${MARIADBCPP_BUILD_DIR}) + set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_BUILD_DIR}/libmariadb) + set(MARIADB_INSTALL_COMMAND INSTALL_COMMAND ${CMAKE_COMMAND} -E true) + endif() + + if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0") + set(MARIADB_POLICY_VERSION_ARG -DCMAKE_POLICY_VERSION_MINIMUM=3.5) + endif() + ExternalProject_Add(mariadb_connector_cpp PREFIX "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp" SOURCE_DIR ${MARIADBCPP_SOURCE_DIR} + BINARY_DIR ${MARIADBCPP_BUILD_DIR} INSTALL_DIR ${MARIADBCPP_INSTALL_DIR} CMAKE_ARGS -Wno-dev + ${MARIADB_POLICY_VERSION_ARG} -DWITH_UNIT_TESTS=OFF -DMARIADB_LINK_DYNAMIC=OFF -DCMAKE_BUILD_RPATH_USE_ORIGIN=${CMAKE_BUILD_RPATH_USE_ORIGIN} @@ -103,6 +120,7 @@ else() # Build from source -DINSTALL_LIBDIR=${MARIADBCPP_LIBRARY_DIR} -DINSTALL_PLUGINDIR=${MARIADBCPP_PLUGIN_DIR} ${MARIADB_EXTRA_CMAKE_ARGS} + ${MARIADB_INSTALL_COMMAND} BUILD_ALWAYS true ) diff --git a/thirdparty/mariadb-connector-cpp b/thirdparty/mariadb-connector-cpp index ef087399..4d0ea2ea 160000 --- a/thirdparty/mariadb-connector-cpp +++ b/thirdparty/mariadb-connector-cpp @@ -1 +1 @@ -Subproject commit ef0873998b3f94a4f76a485fb90b14866fbb99d4 +Subproject commit 4d0ea2ea0cdbba4cface18f1ab38dfcfec9aff8d From 8dfdca7fbd8cacabebd1bdaf6dd617c2d9ff3dfe Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:02:23 -0700 Subject: [PATCH 29/69] feat: add mission progression for behaviors (#1962) * feat: add mission progression for behaviors * Add const to ptr Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 31 +++++++++++++++++-- .../GameDatabase/MySQL/Tables/CharInfo.cpp | 12 +++---- .../GameDatabase/MySQL/Tables/Leaderboard.cpp | 2 +- .../GameDatabase/MySQL/Tables/Property.cpp | 4 +-- dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp | 2 +- dGame/dComponents/ModelComponent.cpp | 4 +++ dGame/dMission/MissionTask.cpp | 2 ++ 7 files changed, 43 insertions(+), 14 deletions(-) diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 456ab5fa..5b26a075 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -9,6 +9,28 @@ typedef std::unique_ptr& UniquePreppedStmtRef; typedef std::unique_ptr UniqueResultSet; +// Holds a PreparedStatement and its ResultSet together to ensure the statement +// outlives the result. +struct PreparedStmtResultSet { + std::unique_ptr m_stmt; + std::unique_ptr m_resultSet; + + PreparedStmtResultSet(sql::PreparedStatement* stmt = nullptr, sql::ResultSet* resultSet = nullptr) + : m_stmt(stmt), m_resultSet(resultSet) {} + + PreparedStmtResultSet(PreparedStmtResultSet&&) = default; + PreparedStmtResultSet& operator=(PreparedStmtResultSet&&) = default; + + ~PreparedStmtResultSet() { + m_resultSet.reset(); + m_stmt.reset(); + } + + sql::ResultSet* operator->() const { + return m_resultSet.get(); + } +}; + // Purposefully no definition for this to provide linker errors in the case someone tries to // bind a parameter to a type that isn't defined. template @@ -136,12 +158,15 @@ private: // Generic query functions that can be used for any query. // Return type may be different depending on the query, so it is up to the caller to check the return type. // The first argument is the query string, and the rest are the parameters to bind to the query. - // The return type is a unique_ptr to the result set, which is deleted automatically when it goes out of scope + // The return type is a PreparedStmtResultSet which keeps the PreparedStatement alive alongside the ResultSet. template - inline std::unique_ptr ExecuteSelect(const std::string& query, Args&&... args) { + inline PreparedStmtResultSet ExecuteSelect(const std::string& query, Args&&... args) { std::unique_ptr preppedStmt(CreatePreppedStmt(query)); SetParams(preppedStmt, std::forward(args)...); - DLU_SQL_TRY_CATCH_RETHROW(return std::unique_ptr(preppedStmt->executeQuery())); + std::unique_ptr resultSet; + DLU_SQL_TRY_CATCH_RETHROW(resultSet.reset(preppedStmt->executeQuery())); + // Release ownership of the pointers to the PreparedStatement and ResultSet to the PreparedStmtResultSet struct, which will ensure they are properly cleaned up. + return PreparedStmtResultSet(preppedStmt.release(), resultSet.release()); } template diff --git a/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp b/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp index 5d744b3d..719a8372 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp @@ -12,7 +12,7 @@ std::vector MySQLDatabase::GetApprovedCharacterNames() { return toReturn; } -std::optional CharInfoFromQueryResult(std::unique_ptr stmt) { +std::optional CharInfoFromQueryResult(PreparedStmtResultSet& stmt) { if (!stmt->next()) { return std::nullopt; } @@ -31,15 +31,13 @@ std::optional CharInfoFromQueryResult(std::unique_ptr MySQLDatabase::GetCharacterInfo(const LWOOBJID charId) { - return CharInfoFromQueryResult( - ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId) - ); + auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId); + return CharInfoFromQueryResult(result); } std::optional MySQLDatabase::GetCharacterInfo(const std::string_view name) { - return CharInfoFromQueryResult( - ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name) - ); + auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name); + return CharInfoFromQueryResult(result); } std::vector MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accountId) { diff --git a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp index eb60bf08..81439c56 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp @@ -14,7 +14,7 @@ std::optional MySQLDatabase::GetDonationTotal(const uint32_t activityI return donation_total->getUInt("donation_total"); } -std::vector ProcessQuery(UniqueResultSet& rows) { +std::vector ProcessQuery(PreparedStmtResultSet& rows) { std::vector entries; entries.reserve(rows->rowsCount()); diff --git a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp index 18916e24..d554a21d 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp @@ -1,7 +1,7 @@ #include "MySQLDatabase.h" #include "ePropertySortType.h" -IProperty::Info ReadPropertyInfo(UniqueResultSet& result) { +IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) { IProperty::Info info; info.id = result->getUInt64("id"); info.ownerId = result->getInt64("owner_id"); @@ -21,7 +21,7 @@ IProperty::Info ReadPropertyInfo(UniqueResultSet& result) { std::optional MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) { std::optional result; std::string query; - std::unique_ptr properties; + PreparedStmtResultSet properties; if (params.sortChoice == SORT_TYPE_FEATURED || params.sortChoice == SORT_TYPE_FRIENDS) { query = R"QUERY( diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 06865c5e..6367560a 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -1,6 +1,6 @@ #include "MySQLDatabase.h" -IUgc::Model ReadModel(UniqueResultSet& result) { +IUgc::Model ReadModel(PreparedStmtResultSet& result) { IUgc::Model model; // blob is owned by the query, so we need to do a deep copy :/ diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 2c975136..4559ca72 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -8,7 +8,9 @@ #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" #include "InventoryComponent.h" +#include "MissionComponent.h" #include "SimplePhysicsComponent.h" +#include "eMissionTaskType.h" #include "eObjectBits.h" #include "Database.h" @@ -187,6 +189,8 @@ void ModelComponent::AddBehavior(AddMessage& msg) { // Check if this behavior is able to be found via lot (if so, its a loot behavior). insertedBehavior.SetIsLoot(inventoryComponent->FindItemByLot(msg.GetBehaviorId(), eInventoryType::BEHAVIORS)); } + auto* const missionComponent = playerEntity->GetComponent(); + if (missionComponent) missionComponent->Progress(eMissionTaskType::ADD_BEHAVIOR, 0); } auto* const simplePhysComponent = m_Parent->GetComponent(); diff --git a/dGame/dMission/MissionTask.cpp b/dGame/dMission/MissionTask.cpp index 801eaeda..c8f2f9c0 100644 --- a/dGame/dMission/MissionTask.cpp +++ b/dGame/dMission/MissionTask.cpp @@ -446,6 +446,8 @@ void MissionTask::Progress(int32_t value, LWOOBJID associate, const std::string& break; } case eMissionTaskType::PLACE_MODEL: + [[fallthrough]]; + case eMissionTaskType::ADD_BEHAVIOR: { AddProgress(count); break; From 247576e101f02eec4d6194627c71ed37cfdb0ac9 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:35:28 -0700 Subject: [PATCH 30/69] fix: use copy ellision (#1963) * use copy ellision tested that the server still starts * Update dDatabase/GameDatabase/MySQL/MySQLDatabase.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 24 +++++++------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 5b26a075..b481877c 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -9,8 +9,8 @@ typedef std::unique_ptr& UniquePreppedStmtRef; typedef std::unique_ptr UniqueResultSet; -// Holds a PreparedStatement and its ResultSet together to ensure the statement -// outlives the result. +// This struct is used to keep the PreparedStatement alive alongside the ResultSet, since the ResultSet will be invalidated if the PreparedStatement is destroyed. +// Declaring the members in reverse order of usage to ensure the PreparedStatement is destroyed after the ResultSet. This is guaranteed by the C++ standard. struct PreparedStmtResultSet { std::unique_ptr m_stmt; std::unique_ptr m_resultSet; @@ -18,14 +18,6 @@ struct PreparedStmtResultSet { PreparedStmtResultSet(sql::PreparedStatement* stmt = nullptr, sql::ResultSet* resultSet = nullptr) : m_stmt(stmt), m_resultSet(resultSet) {} - PreparedStmtResultSet(PreparedStmtResultSet&&) = default; - PreparedStmtResultSet& operator=(PreparedStmtResultSet&&) = default; - - ~PreparedStmtResultSet() { - m_resultSet.reset(); - m_stmt.reset(); - } - sql::ResultSet* operator->() const { return m_resultSet.get(); } @@ -161,12 +153,12 @@ private: // The return type is a PreparedStmtResultSet which keeps the PreparedStatement alive alongside the ResultSet. template inline PreparedStmtResultSet ExecuteSelect(const std::string& query, Args&&... args) { - std::unique_ptr preppedStmt(CreatePreppedStmt(query)); - SetParams(preppedStmt, std::forward(args)...); - std::unique_ptr resultSet; - DLU_SQL_TRY_CATCH_RETHROW(resultSet.reset(preppedStmt->executeQuery())); - // Release ownership of the pointers to the PreparedStatement and ResultSet to the PreparedStmtResultSet struct, which will ensure they are properly cleaned up. - return PreparedStmtResultSet(preppedStmt.release(), resultSet.release()); + PreparedStmtResultSet toReturn; + toReturn.m_stmt.reset(CreatePreppedStmt(query)); + SetParams(toReturn.m_stmt, std::forward(args)...); + DLU_SQL_TRY_CATCH_RETHROW(toReturn.m_resultSet.reset(toReturn.m_stmt->executeQuery())); + // Return the PreparedStmtResultSet, which now owns both the PreparedStatement and ResultSet via unique_ptr and will ensure they are properly cleaned up. + return toReturn; } template From 8061f512aa6c1fcac5c4e94b43ab61e8eb525c08 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:55:57 -0700 Subject: [PATCH 31/69] feat: fix go outside and play mission (#1967) * feat: fix go outside and play mission * Update dWorldServer/WorldServer.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dGame/dComponents/MissionComponent.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * const --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dCommon/dEnums/eCharacterVersion.h | 3 ++- dGame/dComponents/MissionComponent.cpp | 30 +++++++++++++++++++++++++- dGame/dComponents/MissionComponent.h | 2 ++ dGame/dMission/Mission.cpp | 22 ++++++++++++++++--- dGame/dMission/MissionTask.cpp | 19 +++++++++++++++- dWorldServer/WorldServer.cpp | 6 ++++++ 6 files changed, 76 insertions(+), 6 deletions(-) diff --git a/dCommon/dEnums/eCharacterVersion.h b/dCommon/dEnums/eCharacterVersion.h index 15610c3a..7ba3be8f 100644 --- a/dCommon/dEnums/eCharacterVersion.h +++ b/dCommon/dEnums/eCharacterVersion.h @@ -20,7 +20,8 @@ enum class eCharacterVersion : uint32_t { NJ_JAYMISSIONS, NEXUS_FORCE_EXPLORER, // Fixes pet ids in player inventories PET_IDS, // Fixes pet ids in player inventories - UP_TO_DATE, // will become INVENTORY_PERSISTENT_IDS + INVENTORY_PERSISTENT_IDS, // Fixes racing meta missions + UP_TO_DATE, // will become RACING_META_MISSIONS }; #endif //!__ECHARACTERVERSION__H__ diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index b3a503d5..6480e74d 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -3,6 +3,7 @@ * Copyright 2019 */ +#include #include #include @@ -149,7 +150,7 @@ void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID a } for (const auto& [id, mission] : m_Missions) { - if (!mission || std::find(acceptedAchievements.begin(), acceptedAchievements.end(), mission->GetMissionId()) != acceptedAchievements.end()) continue; + if (!mission || std::ranges::find(acceptedAchievements, mission->GetMissionId()) != acceptedAchievements.end()) continue; if (mission->IsAchievement() && ignoreAchievements) continue; @@ -747,3 +748,30 @@ bool MissionComponent::OnMissionNeedsLot(GameMessages::GameMsg& msg) { const auto& needMsg = static_cast(msg); return RequiresItem(needMsg.item); } + +void MissionComponent::FixRacingMetaMissions() { + for (auto* const mission : m_Missions | std::views::values) { + if (!mission || mission->IsComplete()) continue; + + for (auto* const task : mission->GetTasks()) { + if (!task) continue; + + // has to be a racing meta mission and have a taskparam1 of 4 + if (task->GetType() != eMissionTaskType::RACING || !task->GetClientInfo().taskParam1.starts_with("4")) continue; + + // Each target is racing mission that needs to be completed. + // If its completed, progress the meta task by 1. + uint32_t progress = 0; + for (const auto& target : task->GetAllTargets()) { + if (target == 0) continue; + auto* racingMission = GetMission(target); + if (racingMission && racingMission->IsComplete()) { + progress++; + } + } + task->SetProgress(progress); + } + // in case the mission is actually complete, give them the rewards + mission->CheckCompletion(); + } +} diff --git a/dGame/dComponents/MissionComponent.h b/dGame/dComponents/MissionComponent.h index a5cd058a..7ac4b108 100644 --- a/dGame/dComponents/MissionComponent.h +++ b/dGame/dComponents/MissionComponent.h @@ -170,6 +170,8 @@ public: bool HasMission(uint32_t missionId); void ResetMission(const int32_t missionId); + + void FixRacingMetaMissions(); private: bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); bool OnGetMissionState(GameMessages::GameMsg& msg); diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 3d46cde8..e13cc411 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -407,9 +407,7 @@ void Mission::Catchup() { task->Progress(target); } } - } - - if (type == eMissionTaskType::PLAYER_FLAG) { + } else if (type == eMissionTaskType::PLAYER_FLAG) { for (int32_t target : task->GetAllTargets()) { const auto flag = GetCharacter()->GetPlayerFlag(target); @@ -423,6 +421,24 @@ void Mission::Catchup() { break; } } + } else if (type == eMissionTaskType::RACING) { + // check if its a racing meta task ("4") and then set its progress to the current completed missions in all tasks + const auto& clientInfo = task->GetClientInfo(); + if (clientInfo.taskParam1.starts_with("4")) { + // Each target is racing mission that needs to be completed. + // If its completed, progress the meta task by 1. + // at the end set the task progress to avoid sending excess msgs across the wire + uint32_t progress = 0; + for (const auto& target : task->GetAllTargets()) { + if (target == 0) continue; + + auto* racingMission = m_MissionComponent->GetMission(target); + if (racingMission != nullptr && racingMission->IsComplete()) { + progress++; + } + } + task->SetProgress(progress); + } } } } diff --git a/dGame/dMission/MissionTask.cpp b/dGame/dMission/MissionTask.cpp index c8f2f9c0..872796a8 100644 --- a/dGame/dMission/MissionTask.cpp +++ b/dGame/dMission/MissionTask.cpp @@ -418,7 +418,24 @@ void MissionTask::Progress(int32_t value, LWOOBJID associate, const std::string& AddProgress(count); } else if (associate == 4 || associate == 5 || associate == 14) { if (!InAllTargets(value)) break; - AddProgress(count); + if (associate == 4) { + // Recount all completed target missions so this stays idempotent with + // Catchup, which sets the same value on Accept(). AddProgress would + // double-count when LookForAchievements runs Catchup then Progress. + auto* entity = mission->GetAssociate(); + if (entity == nullptr) break; + auto* missionComponent = entity->GetComponent(); + if (missionComponent == nullptr) break; + uint32_t completedCount = 0; + for (const auto& target : GetAllTargets()) { + if (target == 0) continue; + auto* targetMission = missionComponent->GetMission(target); + if (targetMission != nullptr && targetMission->IsComplete()) completedCount++; + } + SetProgress(completedCount); + } else { + AddProgress(count); + } } else if (associate == 17) { if (!InAllTargets(value)) break; AddProgress(count); diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index ad570534..34b485df 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1115,6 +1115,12 @@ void HandlePacket(Packet* packet) { case eCharacterVersion::PET_IDS: { LOG("Regenerating item ids"); inventoryComponent->RegenerateItemIDs(); + levelComponent->SetCharacterVersion(eCharacterVersion::INVENTORY_PERSISTENT_IDS); + [[fallthrough]]; + } + case eCharacterVersion::INVENTORY_PERSISTENT_IDS: { + LOG("Fixing racing meta missions"); + missionComponent->FixRacingMetaMissions(); levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); [[fallthrough]]; } From 482ff82656cbc959e179eb265700e755a6969861 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 14 Apr 2026 01:04:26 -0700 Subject: [PATCH 32/69] fix: adding custom behaviors (#1969) --- dGame/dComponents/ModelComponent.cpp | 8 ++++++-- dGame/dComponents/ModelComponent.h | 2 ++ dGame/dPropertyBehaviors/ControlBehaviors.cpp | 7 ++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 4559ca72..35fc2532 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -189,8 +189,7 @@ void ModelComponent::AddBehavior(AddMessage& msg) { // Check if this behavior is able to be found via lot (if so, its a loot behavior). insertedBehavior.SetIsLoot(inventoryComponent->FindItemByLot(msg.GetBehaviorId(), eInventoryType::BEHAVIORS)); } - auto* const missionComponent = playerEntity->GetComponent(); - if (missionComponent) missionComponent->Progress(eMissionTaskType::ADD_BEHAVIOR, 0); + ProgressAddBehaviorMission(*playerEntity); } auto* const simplePhysComponent = m_Parent->GetComponent(); @@ -200,6 +199,11 @@ void ModelComponent::AddBehavior(AddMessage& msg) { } } +void ModelComponent::ProgressAddBehaviorMission(Entity& playerEntity) { + auto* const missionComponent = playerEntity.GetComponent(); + if (missionComponent) missionComponent->Progress(eMissionTaskType::ADD_BEHAVIOR, 0); +} + std::string ModelComponent::SaveBehavior(const PropertyBehavior& behavior) const { tinyxml2::XMLDocument doc; auto* root = doc.NewElement("Behavior"); diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index 97b165cb..240ddc71 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -98,6 +98,8 @@ public: return msg.GetNeedsNewBehaviorID(); }; + void ProgressAddBehaviorMission(Entity& playerEntity); + void AddBehavior(AddMessage& msg); void RemoveBehavior(MoveToInventoryMessage& msg, const bool keepItem); diff --git a/dGame/dPropertyBehaviors/ControlBehaviors.cpp b/dGame/dPropertyBehaviors/ControlBehaviors.cpp index 70a32981..0b180979 100644 --- a/dGame/dPropertyBehaviors/ControlBehaviors.cpp +++ b/dGame/dPropertyBehaviors/ControlBehaviors.cpp @@ -164,7 +164,12 @@ void ControlBehaviors::ProcessCommand(Entity* const modelEntity, const AMFArrayV LOG("Unknown behavior command (%s)", command.data()); } - if (needsNewBehaviorID) RequestUpdatedID(context); + // If we need a new behaviorID, request it and progress the mission for adding a behavior. + // "add" takes care of this in the ModelComponent directly so we do not need to do it here for that command. + if (needsNewBehaviorID) { + RequestUpdatedID(context); + context.modelComponent->ProgressAddBehaviorMission(*context.modelOwner); + } } ControlBehaviors::ControlBehaviors() { From 67bbe4c1f0475c3ba62f9942be59244e1d9f7603 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 17 May 2026 12:21:08 -0700 Subject: [PATCH 33/69] fix(Missions): mission progression undefined behavior (#1973) * fix: mission progression undefined behavior defer the sub calls until after the loop has finished, that way no ub happens. tested that mission progression all the way up until joining a faction still works and meta missions still function. * default initialize * Update MissionComponent.h --- dGame/dComponents/MissionComponent.cpp | 112 ++++++------------------- dGame/dComponents/MissionComponent.h | 22 ++++- 2 files changed, 47 insertions(+), 87 deletions(-) diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index 6480e74d..e99b1f5e 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -144,13 +144,22 @@ void MissionComponent::RemoveMission(uint32_t missionId) { void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count, bool ignoreAchievements) { LOG("Progressing missions %s %i %llu %s %s", StringifiedEnum::ToString(type).data(), value, associate, targets.c_str(), ignoreAchievements ? "(ignoring achievements)" : ""); - std::vector acceptedAchievements; - if (count > 0 && !ignoreAchievements) { - acceptedAchievements = LookForAchievements(type, value, true, associate, targets, count); + + // If we are already iterating m_Missions, defer this call to avoid iterator invalidation + // from re-entrant insertions (e.g. a completing achievement triggering LookForAchievements). + if (m_IsProgressing) { + m_PendingProgress.push_back({ type, value, associate, targets, count, ignoreAchievements }); + return; } + std::vector acceptedAchievements; + if (count > 0 && !ignoreAchievements) { + acceptedAchievements = LookForAchievements(type, value, associate, targets, count); + } + + m_IsProgressing = true; for (const auto& [id, mission] : m_Missions) { - if (!mission || std::ranges::find(acceptedAchievements, mission->GetMissionId()) != acceptedAchievements.end()) continue; + if (!mission) continue; if (mission->IsAchievement() && ignoreAchievements) continue; @@ -158,6 +167,18 @@ void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID a mission->Progress(type, value, associate, targets, count); } + m_IsProgressing = false; + + // Drain any Progress() calls that were deferred during the loop above. + // Each call here may itself defer further calls, which are drained recursively + // before returning, so the while loop only needs one pass in practice. + while (!m_PendingProgress.empty()) { + auto pending = std::move(m_PendingProgress); + m_PendingProgress.clear(); + for (const auto& p : pending) { + Progress(p.type, p.value, p.associate, p.targets, p.count, p.ignoreAchievements); + } + } } void MissionComponent::ForceProgress(const uint32_t missionId, const uint32_t taskId, const int32_t value, const bool acceptMission) { @@ -280,10 +301,7 @@ bool MissionComponent::GetMissionInfo(uint32_t missionId, CDMissions& result) { return true; } -#define MISSION_NEW_METHOD - -const std::vector MissionComponent::LookForAchievements(eMissionTaskType type, int32_t value, bool progress, LWOOBJID associate, const std::string& targets, int32_t count) { -#ifdef MISSION_NEW_METHOD +const std::vector MissionComponent::LookForAchievements(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count) { // Query for achievments, using the cache const auto& result = QueryAchievements(type, value, targets); @@ -310,85 +328,9 @@ const std::vector MissionComponent::LookForAchievements(eMissionTaskTy instance->Accept(); acceptedAchievements.push_back(missionID); - - if (progress) { - // Progress mission to bring it up to speed - instance->Progress(type, value, associate, targets, count); - } } return acceptedAchievements; -#else - auto* missionTasksTable = CDClientManager::GetTable(); - auto* missionsTable = CDClientManager::GetTable(); - - auto tasks = missionTasksTable->Query([=](const CDMissionTasks& entry) { - return entry.taskType == static_cast(type); - }); - - std::vector acceptedAchievements; - - for (const auto& task : tasks) { - if (GetMission(task.id) != nullptr) { - continue; - } - - const auto missionEntries = missionsTable->Query([=](const CDMissions& entry) { - return entry.id == static_cast(task.id) && !entry.isMission; - }); - - if (missionEntries.empty()) { - continue; - } - - const auto mission = missionEntries[0]; - - if (mission.isMission || !MissionPrerequisites::CanAccept(mission.id, m_Missions)) { - continue; - } - - if (task.target != value && task.targetGroup != targets) { - auto stream = std::istringstream(task.targetGroup); - std::string token; - - auto found = false; - - while (std::getline(stream, token, ',')) { - try { - const auto target = std::stoul(token); - - found = target == value; - - if (found) { - break; - } - } catch (std::invalid_argument& exception) { - LOG("Failed to parse target (%s): (%s)!", token.c_str(), exception.what()); - } - } - - if (!found) { - continue; - } - } - - auto* instance = new Mission(this, mission.id); - - m_Missions.insert_or_assign(mission.id, instance); - - if (instance->IsMission()) instance->SetUniqueMissionOrderID(++m_LastUsedMissionOrderUID); - - instance->Accept(); - - acceptedAchievements.push_back(mission.id); - - if (progress) { - instance->Progress(type, value, associate, targets, count); - } - } - - return acceptedAchievements; -#endif } const std::vector& MissionComponent::QueryAchievements(eMissionTaskType type, int32_t value, const std::string targets) { @@ -497,7 +439,7 @@ bool MissionComponent::RequiresItem(const LOT lot) { } } - const auto required = LookForAchievements(eMissionTaskType::GATHER, lot, false); + const auto required = LookForAchievements(eMissionTaskType::GATHER, lot); return !required.empty(); } diff --git a/dGame/dComponents/MissionComponent.h b/dGame/dComponents/MissionComponent.h index 7ac4b108..f31fac8e 100644 --- a/dGame/dComponents/MissionComponent.h +++ b/dGame/dComponents/MissionComponent.h @@ -134,13 +134,12 @@ public: * Checks if there's any achievements we might be able to accept for the given parameters * @param type the task type for tasks in the achievement that we wish to progress * @param value the value to progress by - * @param progress if we can accept the mission, this will apply the progression * @param associate optional associate related to mission progression * @param targets optional multiple targets related to mission progression * @param count the number of values to progress by (differs by task type) * @return true if a achievement was accepted, false otherwise */ - const std::vector LookForAchievements(eMissionTaskType type, int32_t value, bool progress = true, LWOOBJID associate = LWOOBJID_EMPTY, const std::string& targets = "", int32_t count = 1); + const std::vector LookForAchievements(eMissionTaskType type, int32_t value, LWOOBJID associate = LWOOBJID_EMPTY, const std::string& targets = "", int32_t count = 1); /** * Checks if there's a mission active that requires the collection of the specified LOT @@ -208,6 +207,25 @@ private: * In live this value started at 745. */ uint32_t m_LastUsedMissionOrderUID = 746U; + + /** + * Holds arguments for a Progress() call that arrived re-entrantly while m_Missions was + * being iterated, so it can be replayed after the active loop finishes. + */ + struct PendingProgress { + eMissionTaskType type{}; + int32_t value{}; + LWOOBJID associate{}; + std::string targets; + int32_t count{}; + bool ignoreAchievements{}; + }; + + /// True while the m_Missions range-for loop in Progress() is executing. + bool m_IsProgressing = false; + + /// Re-entrant Progress() calls deferred here for replay after the active loop finishes. + std::vector m_PendingProgress; }; #endif // MISSIONCOMPONENT_H From f5d33a773a6488cf3dc7c7e6ca060b09911d34a8 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 17 May 2026 12:21:22 -0700 Subject: [PATCH 34/69] fix: security fixes (#1974) * fix: security fixes dont print passwords for worlds bound strings from clients actually enable encryption between rakpeers dont allow underflow when reading a string Tested that packets are encrypted tested that models can still be built tested that combat still works * add check * use c++ nullptr instead of NULL * initialize to 0 * globalize constant (should be in a namespace at least in the future) * Update GameMessages.cpp * check bounds --- dCommon/dEnums/dCommonVars.h | 1 + .../dGameMessages/DoClientProjectileImpact.h | 1 + dGame/dGameMessages/EchoStartSkill.h | 1 + dGame/dGameMessages/EchoSyncSkill.h | 1 + dGame/dGameMessages/GameMessages.cpp | 26 ++++++++++++++++++- .../RequestServerProjectileImpact.h | 1 + dGame/dGameMessages/StartSkill.h | 1 + dGame/dGameMessages/SyncSkill.h | 1 + dMasterServer/InstanceManager.cpp | 2 +- dMasterServer/MasterServer.cpp | 4 +-- dNet/AuthPackets.cpp | 2 +- dNet/ClientPackets.cpp | 8 +++--- dNet/dServer.cpp | 3 ++- 13 files changed, 43 insertions(+), 9 deletions(-) diff --git a/dCommon/dEnums/dCommonVars.h b/dCommon/dEnums/dCommonVars.h index f9efa2c6..c00c4be5 100644 --- a/dCommon/dEnums/dCommonVars.h +++ b/dCommon/dEnums/dCommonVars.h @@ -58,6 +58,7 @@ constexpr LWOCLONEID LWOCLONEID_INVALID = -1; //!< Invalid LWOCLONEID constexpr LWOINSTANCEID LWOINSTANCEID_INVALID = -1; //!< Invalid LWOINSTANCEID constexpr LWOMAPID LWOMAPID_INVALID = -1; //!< Invalid LWOMAPID constexpr uint64_t LWOZONEID_INVALID = 0; //!< Invalid LWOZONEID +constexpr uint32_t MAX_MESSAGE_LENGTH = 0x500000; //!< Prevent exceptionally large msgs from being processed. Should always be used to check user provided inputs. constexpr float PI = 3.14159f; diff --git a/dGame/dGameMessages/DoClientProjectileImpact.h b/dGame/dGameMessages/DoClientProjectileImpact.h index c0354e47..16d326fa 100644 --- a/dGame/dGameMessages/DoClientProjectileImpact.h +++ b/dGame/dGameMessages/DoClientProjectileImpact.h @@ -61,6 +61,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/EchoStartSkill.h b/dGame/dGameMessages/EchoStartSkill.h index 0afef3f4..f2229f88 100644 --- a/dGame/dGameMessages/EchoStartSkill.h +++ b/dGame/dGameMessages/EchoStartSkill.h @@ -100,6 +100,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/EchoSyncSkill.h b/dGame/dGameMessages/EchoSyncSkill.h index 5ea866f1..63c23b3e 100644 --- a/dGame/dGameMessages/EchoSyncSkill.h +++ b/dGame/dGameMessages/EchoSyncSkill.h @@ -47,6 +47,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (unsigned int k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 03849d49..8f4fdeb4 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -2405,12 +2405,26 @@ void GameMessages::SendUnSmash(Entity* entity, LWOOBJID builderID, float duratio void GameMessages::HandleControlBehaviors(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { AMFDeserialize reader; - std::unique_ptr amfArguments{ static_cast(reader.Read(inStream).release()) }; + std::unique_ptr amfArguments; + try { + auto deserializedData = reader.Read(inStream); + if (!deserializedData || deserializedData->GetValueType() != eAmf::Array) { + LOG("Failed to deserialize AMF data for control behaviors command: not an array"); + return; + } + + amfArguments.reset(static_cast(deserializedData.release())); + } catch (...) { + LOG("Failed to deserialize AMF data for control behaviors command"); + return; + } if (amfArguments->GetValueType() != eAmf::Array) return; uint32_t commandLength{}; inStream.Read(commandLength); + if (commandLength > MAX_MESSAGE_LENGTH) return; // Prevent DoS via unbounded command buffer + std::string command; command.reserve(commandLength); for (uint32_t i = 0; i < commandLength; ++i) { @@ -3616,6 +3630,8 @@ void GameMessages::HandlePetTamingTryBuild(RakNet::BitStream& inStream, Entity* inStream.Read(brickCount); + if (brickCount > MAX_MESSAGE_LENGTH) return; // Prevent DoS via unbounded brick count + bricks.reserve(brickCount); for (uint32_t i = 0; i < brickCount; i++) { @@ -5806,6 +5822,8 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t messageLength; inStream.Read(messageLength); + if (messageLength > MAX_MESSAGE_LENGTH) return; + for (uint32_t i = 0; i < (messageLength); ++i) { uint16_t character; inStream.Read(character); @@ -5817,6 +5835,7 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t clientVersionLength; inStream.Read(clientVersionLength); + if (clientVersionLength > MAX_MESSAGE_LENGTH) return; for (unsigned int k = 0; k < clientVersionLength; k++) { unsigned char character; inStream.Read(character); @@ -5825,6 +5844,7 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t nOtherPlayerIDLength; inStream.Read(nOtherPlayerIDLength); + if (nOtherPlayerIDLength > MAX_MESSAGE_LENGTH) return; for (unsigned int k = 0; k < nOtherPlayerIDLength; k++) { unsigned char character; inStream.Read(character); @@ -5833,6 +5853,7 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t selectionLength; inStream.Read(selectionLength); + if (selectionLength > MAX_MESSAGE_LENGTH) return; for (unsigned int k = 0; k < selectionLength; k++) { unsigned char character; inStream.Read(character); @@ -6135,14 +6156,17 @@ void GameMessages::HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entit uint32_t size{}; if (!inStream.Read(size)) return; + if (size > MAX_MESSAGE_LENGTH) return; // Bounds check before resize action.resize(size); if (!inStream.Read(action.data(), size)) return; if (!inStream.Read(size)) return; + if (size > MAX_MESSAGE_LENGTH) return; // Bounds check before resize groupUpdate.groupId.resize(size); if (!inStream.Read(groupUpdate.groupId.data(), size)) return; if (!inStream.Read(size)) return; + if (size > MAX_MESSAGE_LENGTH / 2) return; // Bounds check: size * 2 would overflow or exceed limit groupName.resize(size); if (!inStream.Read(reinterpret_cast(groupName.data()), size * 2)) return; diff --git a/dGame/dGameMessages/RequestServerProjectileImpact.h b/dGame/dGameMessages/RequestServerProjectileImpact.h index 394bd9c7..18158399 100644 --- a/dGame/dGameMessages/RequestServerProjectileImpact.h +++ b/dGame/dGameMessages/RequestServerProjectileImpact.h @@ -54,6 +54,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/StartSkill.h b/dGame/dGameMessages/StartSkill.h index 91e35572..6ca51008 100644 --- a/dGame/dGameMessages/StartSkill.h +++ b/dGame/dGameMessages/StartSkill.h @@ -111,6 +111,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/SyncSkill.h b/dGame/dGameMessages/SyncSkill.h index fb5525bc..3128ce91 100644 --- a/dGame/dGameMessages/SyncSkill.h +++ b/dGame/dGameMessages/SyncSkill.h @@ -46,6 +46,7 @@ public: stream.Read(bDone); uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index 2a402f87..c2ab3333 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -308,7 +308,7 @@ const InstancePtr& InstanceManager::FindPrivateInstance(const std::string& passw continue; } - LOG("Password: %s == %s => %d", password.c_str(), instance->GetPassword().c_str(), password == instance->GetPassword()); + LOG("Checking private zone password match (result: %d)", password == instance->GetPassword()); if (instance->GetPassword() == password) { return instance; diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 32a2cb56..aba78800 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -720,7 +720,7 @@ void HandlePacket(Packet* packet) { password += character; } const auto& newInst = Game::im->CreatePrivateInstance(mapId, cloneId, password.c_str()); - LOG("Creating private zone %i/%i/%i with password %s", newInst->GetMapID(), newInst->GetCloneID(), newInst->GetInstanceID(), password.c_str()); + LOG("Creating private zone %i/%i/%i", newInst->GetMapID(), newInst->GetCloneID(), newInst->GetInstanceID()); break; } @@ -747,7 +747,7 @@ void HandlePacket(Packet* packet) { const auto& instance = Game::im->FindPrivateInstance(password.c_str()); - LOG("Join private zone: %llu %d %s %p", requestID, mythranShift, password.c_str(), instance.get()); + LOG("Join private zone: %llu %d %p", requestID, mythranShift, instance.get()); if (instance == nullptr) { return; diff --git a/dNet/AuthPackets.cpp b/dNet/AuthPackets.cpp index a46ed9f1..0473ddf2 100644 --- a/dNet/AuthPackets.cpp +++ b/dNet/AuthPackets.cpp @@ -307,6 +307,6 @@ void AuthPackets::SendLoginResponse(dServer* server, const SystemAddress& sysAdd bitStream.Write(LUString(username)); server->SendToMaster(bitStream); - LOG("Set sessionKey: %i for user %s", sessionKey, username.c_str()); + LOG("Set session key for user %s", username.c_str()); } } diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index a6b9f8c6..41bf1a2d 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -11,14 +11,16 @@ ChatMessage ClientPackets::HandleChatMessage(Packet* packet) { CINSTREAM_SKIP_HEADER; ChatMessage message; - uint32_t messageLength; + int32_t messageLength{}; inStream.Read(message.chatChannel); inStream.Read(message.unknown); inStream.Read(messageLength); - for (uint32_t i = 0; i < (messageLength - 1); ++i) { - uint16_t character; + if (messageLength > MAX_MESSAGE_LENGTH || messageLength < 0) return message; + + for (int32_t i = 0; i < (messageLength - 1); ++i) { + char16_t character; inStream.Read(character); message.message.push_back(character); } diff --git a/dNet/dServer.cpp b/dNet/dServer.cpp index 0a8e0ab9..53009d90 100644 --- a/dNet/dServer.cpp +++ b/dNet/dServer.cpp @@ -215,6 +215,8 @@ bool dServer::Startup() { mPeer = RakNetworkFactory::GetRakPeerInterface(); if (!mPeer) return false; + + if (mUseEncryption) mPeer->InitializeSecurity(nullptr, nullptr, nullptr, nullptr); if (!mPeer->Startup(mMaxConnections, 10, &mSocketDescriptor, 1)) return false; if (mIsInternal) { @@ -226,7 +228,6 @@ bool dServer::Startup() { } mPeer->SetMaximumIncomingConnections(mMaxConnections); - if (mUseEncryption) mPeer->InitializeSecurity(NULL, NULL, NULL, NULL); return true; } From f3a5add038ec29ce59069eadbd67cd99b9dad5b6 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 18 May 2026 03:15:03 -0700 Subject: [PATCH 35/69] fix(tac arc): incorrect check causing any enemy lower than you to not attack you (#1975) tested that close range enemies above and below you now correctly attack you --- dGame/dBehaviors/TacArcBehavior.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dGame/dBehaviors/TacArcBehavior.cpp b/dGame/dBehaviors/TacArcBehavior.cpp index d0bbad8e..616651f5 100644 --- a/dGame/dBehaviors/TacArcBehavior.cpp +++ b/dGame/dBehaviors/TacArcBehavior.cpp @@ -119,9 +119,8 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS const auto targetPos = validTarget->GetPosition(); - // make sure we aren't too high or low in comparison to the targer - const auto heightDifference = std::abs(reference.y - targetPos.y); - if (targetPos.y > reference.y && heightDifference > this->m_upperBound || targetPos.y < reference.y && heightDifference > this->m_lowerBound) + // make sure we aren't too high or low in comparison to the target + if (targetPos.y > (reference.y + m_upperBound) || targetPos.y < (reference.y + m_lowerBound)) continue; const auto forward = QuatUtils::Forward(self->GetRotation()); @@ -210,7 +209,7 @@ void TacArcBehavior::Load() { ); this->m_method = GetInt("method", 1); this->m_upperBound = std::abs(GetFloat("upper_bound", 4.4f)); - this->m_lowerBound = std::abs(GetFloat("lower_bound", 0.4f)); + this->m_lowerBound = std::abs(GetFloat("lower_bound", 0.4f)) - 5.0f; // Makes it so players and objects can still be targetted when slightly below the caster. FIXME: use bounding spheres at some point this->m_usePickedTarget = GetBoolean("use_picked_target", false); this->m_useTargetPostion = GetBoolean("use_target_position", false); this->m_checkEnv = GetBoolean("check_env", false); From 4ef9f4326679803dd1ea8317736f4717366b4c5d Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 19 May 2026 11:42:56 -0700 Subject: [PATCH 36/69] feat: make gm registration simpler and safer (#1932) * gm registration re-work * fix errors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove duplicate message * Remove duplicate function * add null check --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dGame/Entity.cpp | 31 ++++----- dGame/Entity.h | 39 +++++++----- dGame/dComponents/ActivityComponent.cpp | 7 +-- dGame/dComponents/ActivityComponent.h | 2 +- dGame/dComponents/BaseCombatAIComponent.cpp | 10 +-- dGame/dComponents/BaseCombatAIComponent.h | 2 +- dGame/dComponents/BouncerComponent.cpp | 10 +-- dGame/dComponents/BouncerComponent.h | 2 +- dGame/dComponents/CharacterComponent.cpp | 5 +- dGame/dComponents/CharacterComponent.h | 6 +- dGame/dComponents/CollectibleComponent.cpp | 6 +- dGame/dComponents/CollectibleComponent.h | 2 +- dGame/dComponents/Component.h | 19 +++--- .../ControllablePhysicsComponent.cpp | 7 +-- .../ControllablePhysicsComponent.h | 2 +- dGame/dComponents/DestroyableComponent.cpp | 20 +++--- dGame/dComponents/DestroyableComponent.h | 8 ++- dGame/dComponents/GhostComponent.cpp | 22 +++---- dGame/dComponents/GhostComponent.h | 6 +- .../HavokVehiclePhysicsComponent.cpp | 7 +-- .../HavokVehiclePhysicsComponent.h | 2 +- dGame/dComponents/InventoryComponent.cpp | 10 ++- dGame/dComponents/InventoryComponent.h | 6 +- dGame/dComponents/MissionComponent.cpp | 28 ++++----- dGame/dComponents/MissionComponent.h | 6 +- dGame/dComponents/ModelComponent.cpp | 20 +++--- dGame/dComponents/ModelComponent.h | 6 +- dGame/dComponents/MovementAIComponent.cpp | 63 +++++++++++++++++++ dGame/dComponents/MovementAIComponent.h | 1 + dGame/dComponents/PhantomPhysicsComponent.cpp | 7 +-- dGame/dComponents/PhantomPhysicsComponent.h | 2 +- dGame/dComponents/PhysicsComponent.cpp | 9 ++- dGame/dComponents/PhysicsComponent.h | 5 +- .../RigidbodyPhantomPhysicsComponent.cpp | 7 +-- .../RigidbodyPhantomPhysicsComponent.h | 2 +- dGame/dComponents/ScriptComponent.cpp | 8 +-- dGame/dComponents/ScriptComponent.h | 2 +- dGame/dComponents/SimplePhysicsComponent.cpp | 7 +-- dGame/dComponents/SimplePhysicsComponent.h | 2 +- dGame/dGameMessages/GameMessages.h | 12 ++-- 40 files changed, 232 insertions(+), 186 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index f4ad52f3..e9e4b4e8 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -198,11 +198,12 @@ Entity::~Entity() { } void Entity::Initialize() { - RegisterMsg(this, &Entity::MsgRequestServerObjectInfo); - RegisterMsg(this, &Entity::MsgDropClientLoot); - RegisterMsg(this, &Entity::MsgGetFactionTokenType); - RegisterMsg(this, &Entity::MsgPickupItem); - RegisterMsg(this, &Entity::MsgChildRemoved); + RegisterMsg(&Entity::MsgRequestServerObjectInfo); + RegisterMsg(&Entity::MsgDropClientLoot); + RegisterMsg(&Entity::MsgGetFactionTokenType); + RegisterMsg(&Entity::MsgPickupItem); + RegisterMsg(&Entity::MsgChildRemoved); + RegisterMsg(&Entity::MsgGetFlag); /** * Setup trigger */ @@ -2251,8 +2252,7 @@ void Entity::RegisterMsg(const MessageType::Game msgId, std::function(msg); +bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& requestInfo) { AMFArrayValue response; response.Insert("visible", true); response.Insert("objectID", std::to_string(m_ObjectID)); @@ -2288,9 +2288,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) { return true; } -bool Entity::MsgDropClientLoot(GameMessages::GameMsg& msg) { - auto& dropLootMsg = static_cast(msg); - +bool Entity::MsgDropClientLoot(GameMessages::DropClientLoot& dropLootMsg) { if (dropLootMsg.item != LOT_NULL && dropLootMsg.item != 0) { Loot::Info info{ .id = dropLootMsg.lootID, @@ -2307,13 +2305,11 @@ bool Entity::MsgDropClientLoot(GameMessages::GameMsg& msg) { return true; } -bool Entity::MsgGetFlag(GameMessages::GameMsg& msg) { - auto& flagMsg = static_cast(msg); +bool Entity::MsgGetFlag(GameMessages::GetFlag& flagMsg) { if (m_Character) flagMsg.flag = m_Character->GetPlayerFlag(flagMsg.flagID); return true; } -bool Entity::MsgGetFactionTokenType(GameMessages::GameMsg& msg) { - auto& tokenMsg = static_cast(msg); +bool Entity::MsgGetFactionTokenType(GameMessages::GetFactionTokenType& tokenMsg) { GameMessages::GetFlag getFlagMsg{}; getFlagMsg.flagID = ePlayerFlag::ASSEMBLY_FACTION; @@ -2336,8 +2332,7 @@ bool Entity::MsgGetFactionTokenType(GameMessages::GameMsg& msg) { return tokenMsg.tokenType != LOT_NULL; } -bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) { - auto& pickupItemMsg = static_cast(msg); +bool Entity::MsgPickupItem(GameMessages::PickupItem& pickupItemMsg) { if (GetObjectID() == pickupItemMsg.lootOwnerID) { PickupItem(pickupItemMsg.lootID); } else { @@ -2358,7 +2353,7 @@ bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) { return true; } -bool Entity::MsgChildRemoved(GameMessages::GameMsg& msg) { - GetScript()->OnChildRemoved(*this, static_cast(msg)); +bool Entity::MsgChildRemoved(GameMessages::ChildRemoved& msg) { + GetScript()->OnChildRemoved(*this, msg); return true; } diff --git a/dGame/Entity.h b/dGame/Entity.h index 5310cd0f..565c906c 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -20,6 +20,12 @@ namespace GameMessages { struct ShootingGalleryFire; struct ChildLoaded; struct PlayerResurrectionFinished; + struct RequestServerObjectInfo; + struct DropClientLoot; + struct GetFlag; + struct GetFactionTokenType; + struct PickupItem; + struct ChildRemoved; }; namespace MessageType { @@ -175,12 +181,12 @@ public: void AddComponent(eReplicaComponentType componentId, Component* component); - bool MsgRequestServerObjectInfo(GameMessages::GameMsg& msg); - bool MsgDropClientLoot(GameMessages::GameMsg& msg); - bool MsgGetFlag(GameMessages::GameMsg& msg); - bool MsgGetFactionTokenType(GameMessages::GameMsg& msg); - bool MsgPickupItem(GameMessages::GameMsg& msg); - bool MsgChildRemoved(GameMessages::GameMsg& msg); + bool MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& msg); + bool MsgDropClientLoot(GameMessages::DropClientLoot& msg); + bool MsgGetFlag(GameMessages::GetFlag& msg); + bool MsgGetFactionTokenType(GameMessages::GetFactionTokenType& msg); + bool MsgPickupItem(GameMessages::PickupItem& msg); + bool MsgChildRemoved(GameMessages::ChildRemoved& msg); // This is expceted to never return nullptr, an assert checks this. CppScripts::Script* const GetScript() const; @@ -343,14 +349,19 @@ public: bool HandleMsg(GameMessages::GameMsg& msg) const; - void RegisterMsg(const MessageType::Game msgId, auto* self, const auto handler) { - RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1)); - } - - template - inline void RegisterMsg(auto* self, const auto handler) { - T msg; - RegisterMsg(msg.msgId, self, handler); + // Provided a function that has a derived GameMessage as its only argument and returns a boolean, + // this will register it as a handler for that message type. Casting is done automatically to the type + // of the message in the first argument. This object is expected to exist as long as the handler can be called. + template + inline void RegisterMsg(bool (Entity::* handler)(DerivedGameMsg&)) { + static_assert(std::is_base_of_v, "DerivedGameMsg must inherit from GameMsg"); + const auto boundFunction = std::bind(handler, this, std::placeholders::_1); + // This is the actual function that will be registered, which casts the base GameMsg to the derived type + const auto castWrapper = [boundFunction](GameMessages::GameMsg& msg) { + return boundFunction(static_cast(msg)); + }; + DerivedGameMsg msg; + RegisterMsg(msg.msgId, castWrapper); } /** diff --git a/dGame/dComponents/ActivityComponent.cpp b/dGame/dComponents/ActivityComponent.cpp index c5436df8..2dfd5e95 100644 --- a/dGame/dComponents/ActivityComponent.cpp +++ b/dGame/dComponents/ActivityComponent.cpp @@ -31,8 +31,7 @@ #include "Amf3.h" ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; - RegisterMsg(this, &ActivityComponent::OnGetObjectReportInfo); + RegisterMsg(&ActivityComponent::OnGetObjectReportInfo); /* * This is precisely what the client does functionally * Use the component id as the default activity id and load its data from the database @@ -590,9 +589,7 @@ Entity* LobbyPlayer::GetEntity() const { return Game::entityManager->GetEntity(entityID); } -bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); - +bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& activityInfo = reportInfo.info->PushDebug("Activity"); auto& instances = activityInfo.PushDebug("Instances: " + std::to_string(m_Instances.size())); diff --git a/dGame/dComponents/ActivityComponent.h b/dGame/dComponents/ActivityComponent.h index bcaa5bfc..4f423d8e 100644 --- a/dGame/dComponents/ActivityComponent.h +++ b/dGame/dComponents/ActivityComponent.h @@ -347,7 +347,7 @@ public: private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); /** * The database information for this activity */ diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index 73118e36..2bad800b 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -30,10 +30,7 @@ #include "Amf3.h" BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - { - using namespace GameMessages; - RegisterMsg(this, &BaseCombatAIComponent::MsgGetObjectReportInfo); - } + RegisterMsg(&BaseCombatAIComponent::MsgGetObjectReportInfo); m_Target = LWOOBJID_EMPTY; m_DirtyStateOrTarget = true; m_State = AiState::spawn; @@ -845,10 +842,9 @@ void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float valu m_Target = LWOOBJID_EMPTY; } -bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { +bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { using enum AiState; - auto& reportMsg = static_cast(msg); - auto& cmptType = reportMsg.info->PushDebug("Base Combat AI"); + 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); diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 009a96d2..95eb75ce 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -234,7 +234,7 @@ public: // Ignore a threat for a certain amount of time void IgnoreThreat(const LWOOBJID target, const float time); - bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); private: /** diff --git a/dGame/dComponents/BouncerComponent.cpp b/dGame/dComponents/BouncerComponent.cpp index 3c535e43..3f0ffc9a 100644 --- a/dGame/dComponents/BouncerComponent.cpp +++ b/dGame/dComponents/BouncerComponent.cpp @@ -31,10 +31,7 @@ BouncerComponent::BouncerComponent(Entity* parent, const int32_t componentID) : LookupPetSwitch(); } - { - using namespace GameMessages; - RegisterMsg(this, &BouncerComponent::MsgGetObjectReportInfo); - } + RegisterMsg(&BouncerComponent::MsgGetObjectReportInfo); } BouncerComponent::~BouncerComponent() { @@ -113,9 +110,8 @@ void BouncerComponent::LookupPetSwitch() { } } -bool BouncerComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportMsg = static_cast(msg); - auto& cmptType = reportMsg.info->PushDebug("Bouncer"); +bool BouncerComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& cmptType = reportInfo.info->PushDebug("Bouncer"); cmptType.PushDebug("Component ID") = GetComponentID(); auto& destPos = cmptType.PushDebug("Destination Position"); if (m_Destination != NiPoint3Constant::ZERO) { diff --git a/dGame/dComponents/BouncerComponent.h b/dGame/dComponents/BouncerComponent.h index b3221e12..f0493ee9 100644 --- a/dGame/dComponents/BouncerComponent.h +++ b/dGame/dComponents/BouncerComponent.h @@ -51,7 +51,7 @@ public: */ void LookupPetSwitch(); - bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); private: /** diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index 54708226..dda9d559 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -49,11 +49,10 @@ CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID m_LastUpdateTimestamp = std::time(nullptr); m_SystemAddress = systemAddress; - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &CharacterComponent::OnGetObjectReportInfo); + RegisterMsg(&CharacterComponent::OnGetObjectReportInfo); } -bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); +bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& cmptType = reportInfo.info->PushDebug("Character"); diff --git a/dGame/dComponents/CharacterComponent.h b/dGame/dComponents/CharacterComponent.h index c1f107b5..6f5eeaab 100644 --- a/dGame/dComponents/CharacterComponent.h +++ b/dGame/dComponents/CharacterComponent.h @@ -17,6 +17,10 @@ enum class eGameActivity : uint32_t; class Item; +namespace GameMessages { + struct GetObjectReportInfo; +} + /** * The statistics that can be achieved per zone */ @@ -331,7 +335,7 @@ public: void LoadVisitedLevelsXml(const tinyxml2::XMLElement& doc); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * The map of active venture vision effects diff --git a/dGame/dComponents/CollectibleComponent.cpp b/dGame/dComponents/CollectibleComponent.cpp index fce32e93..10b50c2d 100644 --- a/dGame/dComponents/CollectibleComponent.cpp +++ b/dGame/dComponents/CollectibleComponent.cpp @@ -6,16 +6,14 @@ CollectibleComponent::CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) : Component(parentEntity, componentID), m_CollectibleId(collectibleId) { - using namespace GameMessages; - RegisterMsg(this, &CollectibleComponent::MsgGetObjectReportInfo); + RegisterMsg(&CollectibleComponent::MsgGetObjectReportInfo); } void CollectibleComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) { outBitStream.Write(GetCollectibleId()); } -bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportMsg = static_cast(msg); +bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) { auto& cmptType = reportMsg.info->PushDebug("Collectible"); auto collectibleID = static_cast(m_CollectibleId) + static_cast(Game::server->GetZoneID() << 8); diff --git a/dGame/dComponents/CollectibleComponent.h b/dGame/dComponents/CollectibleComponent.h index ba1a3f28..5c9b5ea0 100644 --- a/dGame/dComponents/CollectibleComponent.h +++ b/dGame/dComponents/CollectibleComponent.h @@ -12,7 +12,7 @@ public: int16_t GetCollectibleId() const { return m_CollectibleId; } void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) override; - bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); private: int16_t m_CollectibleId = 0; }; diff --git a/dGame/dComponents/Component.h b/dGame/dComponents/Component.h index b54a8fab..094ed9f3 100644 --- a/dGame/dComponents/Component.h +++ b/dGame/dComponents/Component.h @@ -55,17 +55,18 @@ public: virtual void LoadFromXml(const tinyxml2::XMLDocument& doc) {} virtual void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {} - protected: + template + inline void RegisterMsg(bool (GameObjClass::*handler)(DerivedMsg&)) { + static_assert(std::is_base_of_v, "DerivedMsg must inherit from GameMsg"); + static_assert(std::is_base_of_v, "GameObjClass must inherit from Component"); + const auto handlerBound = std::bind(handler, static_cast(this), std::placeholders::_1); + const auto castWrapper = [handlerBound](GameMessages::GameMsg& msg) { + return handlerBound(static_cast(msg)); + }; - inline void RegisterMsg(const MessageType::Game msgId, auto* self, const auto handler) { - m_Parent->RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1)); - } - - template - inline void RegisterMsg(auto* self, const auto handler) { - T msg; - RegisterMsg(msg.msgId, self, handler); + DerivedMsg msg; + m_Parent->RegisterMsg(msg.msgId, castWrapper); } /** diff --git a/dGame/dComponents/ControllablePhysicsComponent.cpp b/dGame/dComponents/ControllablePhysicsComponent.cpp index b2a41358..2b26368f 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/ControllablePhysicsComponent.cpp @@ -18,7 +18,7 @@ #include "Amf3.h" ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity, const int32_t componentID) : PhysicsComponent(entity, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &ControllablePhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&ControllablePhysicsComponent::OnGetObjectReportInfo); m_Velocity = {}; m_AngularVelocity = {}; @@ -359,9 +359,8 @@ void ControllablePhysicsComponent::SetStunImmunity( ); } -bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); auto& info = reportInfo.subCategory->PushDebug("Controllable Info"); auto& vel = info.PushDebug("Velocity"); diff --git a/dGame/dComponents/ControllablePhysicsComponent.h b/dGame/dComponents/ControllablePhysicsComponent.h index 419e9250..76de5b51 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.h +++ b/dGame/dComponents/ControllablePhysicsComponent.h @@ -284,7 +284,7 @@ public: private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * The entity that owns this component */ diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 0f886d84..b4250966 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -48,7 +48,6 @@ Implementation DestroyableComponent::IsEnemyImplentation; Implementation DestroyableComponent::IsFriendImplentation; DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; m_iArmor = 0; m_fMaxArmor = 0.0f; m_iImagination = 0; @@ -86,9 +85,9 @@ DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t compone m_DamageCooldownTimer = 0.0f; - RegisterMsg(this, &DestroyableComponent::OnGetObjectReportInfo); - RegisterMsg(this, &DestroyableComponent::OnSetFaction); - RegisterMsg(this, &DestroyableComponent::OnIsDead); + RegisterMsg(&DestroyableComponent::OnGetObjectReportInfo); + RegisterMsg(&DestroyableComponent::OnSetFaction); + RegisterMsg(&DestroyableComponent::OnIsDead); } DestroyableComponent::~DestroyableComponent() { @@ -1061,8 +1060,7 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) { } } -bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); +bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& destroyableInfo = reportInfo.info->PushDebug("Destroyable"); destroyableInfo.PushDebug("DestructibleComponent DB Table Template ID") = m_ComponentID; @@ -1184,16 +1182,14 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { return true; } -bool DestroyableComponent::OnSetFaction(GameMessages::GameMsg& msg) { - auto& modifyFaction = static_cast(msg); +bool DestroyableComponent::OnSetFaction(GameMessages::SetFaction& setFaction) { m_DirtyHealth = true; Game::entityManager->SerializeEntity(m_Parent); - SetFaction(modifyFaction.factionID, modifyFaction.bIgnoreChecks); + SetFaction(setFaction.factionID, setFaction.bIgnoreChecks); return true; } -bool DestroyableComponent::OnIsDead(GameMessages::GameMsg& msg) { - auto& isDeadMsg = static_cast(msg); - isDeadMsg.bDead = m_IsDead || (GetHealth() == 0 && GetArmor() == 0); +bool DestroyableComponent::OnIsDead(GameMessages::IsDead& isDead) { + isDead.bDead = m_IsDead || (GetHealth() == 0 && GetArmor() == 0); return true; } diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 0b4d2ba8..7ef55a21 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -11,6 +11,8 @@ namespace GameMessages { struct GetObjectReportInfo; + struct SetFaction; + struct IsDead; }; namespace CppScripts { @@ -470,9 +472,9 @@ public: // handle hardcode mode drops void DoHardcoreModeDrops(const LWOOBJID source); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); - bool OnSetFaction(GameMessages::GameMsg& msg); - bool OnIsDead(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + bool OnSetFaction(GameMessages::SetFaction& setFaction); + bool OnIsDead(GameMessages::IsDead& isDead); void SetIsDead(const bool value) { m_IsDead = value; } diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index d5f3b9bc..8223080d 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -13,9 +13,9 @@ GhostComponent::GhostComponent(Entity* parent, const int32_t componentID) : Comp m_GhostOverridePoint = NiPoint3Constant::ZERO; m_GhostOverride = false; - RegisterMsg(this, &GhostComponent::OnToggleGMInvis); - RegisterMsg(this, &GhostComponent::OnGetGMInvis); - RegisterMsg(this, &GhostComponent::MsgGetObjectReportInfo); + RegisterMsg(&GhostComponent::OnToggleGMInvis); + RegisterMsg(&GhostComponent::OnGetGMInvis); + RegisterMsg(&GhostComponent::MsgGetObjectReportInfo); } GhostComponent::~GhostComponent() { @@ -88,15 +88,17 @@ void GhostComponent::GhostEntity(LWOOBJID id) { m_ObservedEntities.erase(id); } -bool GhostComponent::OnToggleGMInvis(GameMessages::GameMsg& msg) { - // TODO: disabled for now while bugs are fixed - return false; - auto& gmInvisMsg = static_cast(msg); +bool GhostComponent::OnToggleGMInvis(GameMessages::ToggleGMInvis& gmInvisMsg) { gmInvisMsg.bStateOut = !m_IsGMInvisible; m_IsGMInvisible = !m_IsGMInvisible; LOG_DEBUG("GM Invisibility toggled to: %s", m_IsGMInvisible ? "true" : "false"); gmInvisMsg.Send(UNASSIGNED_SYSTEM_ADDRESS); auto* thisUser = UserManager::Instance()->GetUser(m_Parent->GetSystemAddress()); + if (!thisUser) { + LOG("Unable to find user for entity %llu when toggling GM invisibility!", m_Parent->GetObjectID()); + return false; + } + for (const auto& player : PlayerManager::GetAllPlayers()) { if (!player || player->GetObjectID() == m_Parent->GetObjectID()) continue; auto* toUser = UserManager::Instance()->GetUser(player->GetSystemAddress()); @@ -117,9 +119,8 @@ bool GhostComponent::OnToggleGMInvis(GameMessages::GameMsg& msg) { return true; } -bool GhostComponent::OnGetGMInvis(GameMessages::GameMsg& msg) { +bool GhostComponent::OnGetGMInvis(GameMessages::GetGMInvis& gmInvisMsg) { LOG_DEBUG("GM Invisibility requested: %s", m_IsGMInvisible ? "true" : "false"); - auto& gmInvisMsg = static_cast(msg); // TODO: disabled for now while bugs are fixed // gmInvisMsg.bGMInvis = m_IsGMInvisible; // return gmInvisMsg.bGMInvis; @@ -127,8 +128,7 @@ bool GhostComponent::OnGetGMInvis(GameMessages::GameMsg& msg) { return false; } -bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportMsg = static_cast(msg); +bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) { auto& cmptType = reportMsg.info->PushDebug("Ghost"); cmptType.PushDebug("Component ID") = GetComponentID(); cmptType.PushDebug("Is GM Invis") = false; diff --git a/dGame/dComponents/GhostComponent.h b/dGame/dComponents/GhostComponent.h index 8ea05397..4bd3318a 100644 --- a/dGame/dComponents/GhostComponent.h +++ b/dGame/dComponents/GhostComponent.h @@ -45,11 +45,11 @@ public: void GhostEntity(const LWOOBJID id); - bool OnToggleGMInvis(GameMessages::GameMsg& msg); + bool OnToggleGMInvis(GameMessages::ToggleGMInvis& msg); - bool OnGetGMInvis(GameMessages::GameMsg& msg); + bool OnGetGMInvis(GameMessages::GetGMInvis& msg); - bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg); + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); private: diff --git a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp index afec4427..52819657 100644 --- a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp +++ b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp @@ -3,7 +3,7 @@ #include "Amf3.h" HavokVehiclePhysicsComponent::HavokVehiclePhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &HavokVehiclePhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&HavokVehiclePhysicsComponent::OnGetObjectReportInfo); m_Velocity = NiPoint3Constant::ZERO; m_AngularVelocity = NiPoint3Constant::ZERO; @@ -102,9 +102,8 @@ void HavokVehiclePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bo outBitStream.Write0(); } -bool HavokVehiclePhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool HavokVehiclePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); if (!reportInfo.subCategory) { return false; } diff --git a/dGame/dComponents/HavokVehiclePhysicsComponent.h b/dGame/dComponents/HavokVehiclePhysicsComponent.h index 3a84adca..3b2aba78 100644 --- a/dGame/dComponents/HavokVehiclePhysicsComponent.h +++ b/dGame/dComponents/HavokVehiclePhysicsComponent.h @@ -68,7 +68,7 @@ public: void SetRemoteInputInfo(const RemoteInputInfo&); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); NiPoint3 m_Velocity; NiPoint3 m_AngularVelocity; diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 212572ae..2ff827d8 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -44,8 +44,7 @@ #include InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; - RegisterMsg(this, &InventoryComponent::OnGetObjectReportInfo); + RegisterMsg(&InventoryComponent::OnGetObjectReportInfo); this->m_Dirty = true; this->m_Equipped = {}; this->m_Pushed = {}; @@ -1847,9 +1846,8 @@ std::string DebugInvToString(const eInventoryType inv, bool verbose) { return ""; } -bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& report = static_cast(msg); - auto& cmpt = report.info->PushDebug("Inventory"); +bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& cmpt = reportInfo.info->PushDebug("Inventory"); cmpt.PushDebug("Component ID") = GetComponentID(); uint32_t numItems = 0; for (auto* inventory : m_Inventories | std::views::values) numItems += inventory->GetItems().size(); @@ -1859,7 +1857,7 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { for (const auto& [id, inventoryMut] : m_Inventories) { if (!inventoryMut) continue; const auto* const inventory = inventoryMut; - auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, report.bVerbose) + " - " + std::to_string(id)); + auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, reportInfo.bVerbose) + " - " + std::to_string(id)); for (uint32_t i = 0; i < inventory->GetSize(); i++) { const auto* const item = inventory->FindItemBySlot(i); if (!item) continue; diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 3728c428..beaf0efb 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -31,6 +31,10 @@ typedef std::map EquipmentMap; enum class eItemType : int32_t; +namespace GameMessages { + struct GetObjectReportInfo; +} + /** * Handles the inventory of entity, including the items they possess and have equipped. An entity can have inventories * of different types, each type representing a different group of items, see `eInventoryType` for a list of @@ -411,7 +415,7 @@ public: // Used to migrate a character version, no need to call outside of that context void RegenerateItemIDs(); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); ~InventoryComponent() override; diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index e99b1f5e..e7c03379 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -28,12 +28,11 @@ std::unordered_map> MissionComponent: //! Initializer MissionComponent::MissionComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue(); - RegisterMsg(this, &MissionComponent::OnGetObjectReportInfo); - RegisterMsg(this, &MissionComponent::OnGetMissionState); - RegisterMsg(this, &MissionComponent::OnMissionNeedsLot); + RegisterMsg(&MissionComponent::OnGetObjectReportInfo); + RegisterMsg(&MissionComponent::OnGetMissionState); + RegisterMsg(&MissionComponent::OnMissionNeedsLot); } //! Destructor @@ -649,9 +648,8 @@ void PushMissions(const std::map& missions, AMFArrayValue& V } } -bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportMsg = static_cast(msg); - auto& missionInfo = reportMsg.info->PushDebug("Mission (Laggy)"); +bool MissionComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& missionInfo = reportInfo.info->PushDebug("Mission (Laggy)"); missionInfo.PushDebug("Component ID") = GetComponentID(); // Sort the missions so they are easier to parse and present to the end user std::map achievements; @@ -667,28 +665,26 @@ bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { // None of these should be empty, but if they are dont print the field if (!achievements.empty() || !missions.empty()) { auto& incompleteMissions = missionInfo.PushDebug("Incomplete Missions"); - PushMissions(achievements, incompleteMissions, reportMsg.bVerbose); - PushMissions(missions, incompleteMissions, reportMsg.bVerbose); + PushMissions(achievements, incompleteMissions, reportInfo.bVerbose); + PushMissions(missions, incompleteMissions, reportInfo.bVerbose); } if (!doneMissions.empty()) { auto& completeMissions = missionInfo.PushDebug("Completed Missions"); - PushMissions(doneMissions, completeMissions, reportMsg.bVerbose); + PushMissions(doneMissions, completeMissions, reportInfo.bVerbose); } return true; } -bool MissionComponent::OnGetMissionState(GameMessages::GameMsg& msg) { - auto misState = static_cast(msg); - misState.missionState = GetMissionState(misState.missionID); +bool MissionComponent::OnGetMissionState(GameMessages::GetMissionState& getMissionState) { + getMissionState.missionState = GetMissionState(getMissionState.missionID); return true; } -bool MissionComponent::OnMissionNeedsLot(GameMessages::GameMsg& msg) { - const auto& needMsg = static_cast(msg); - return RequiresItem(needMsg.item); +bool MissionComponent::OnMissionNeedsLot(GameMessages::MissionNeedsLot& missionNeedsLot) { + return RequiresItem(missionNeedsLot.item); } void MissionComponent::FixRacingMetaMissions() { diff --git a/dGame/dComponents/MissionComponent.h b/dGame/dComponents/MissionComponent.h index f31fac8e..8545b171 100644 --- a/dGame/dComponents/MissionComponent.h +++ b/dGame/dComponents/MissionComponent.h @@ -172,9 +172,9 @@ public: void FixRacingMetaMissions(); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); - bool OnGetMissionState(GameMessages::GameMsg& msg); - bool OnMissionNeedsLot(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + bool OnGetMissionState(GameMessages::GetMissionState& getMissionState); + bool OnMissionNeedsLot(GameMessages::MissionNeedsLot& missionNeedsLot); /** * All the missions owned by this entity, mapped by mission ID */ diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 35fc2532..14093b15 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -17,20 +17,18 @@ #include "DluAssert.h" ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; m_OriginalPosition = m_Parent->GetDefaultPosition(); m_OriginalRotation = m_Parent->GetDefaultRotation(); m_IsPaused = false; m_NumListeningInteract = 0; m_userModelID = m_Parent->GetVarAs(u"userModelID"); - RegisterMsg(this, &ModelComponent::OnRequestUse); - RegisterMsg(this, &ModelComponent::OnResetModelToDefaults); - RegisterMsg(this, &ModelComponent::OnGetObjectReportInfo); + RegisterMsg(&ModelComponent::OnRequestUse); + RegisterMsg(&ModelComponent::OnResetModelToDefaults); + RegisterMsg(&ModelComponent::OnGetObjectReportInfo); } -bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { - auto& reset = static_cast(msg); +bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults& reset) { if (reset.bResetBehaviors) for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset); if (reset.bUnSmash) { @@ -61,10 +59,9 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { return true; } -bool ModelComponent::OnRequestUse(GameMessages::GameMsg& msg) { +bool ModelComponent::OnRequestUse(GameMessages::RequestUse& requestUse) { bool toReturn = false; if (!m_IsPaused) { - auto& requestUse = static_cast(msg); for (auto& behavior : m_Behaviors) behavior.HandleMsg(requestUse); toReturn = true; } @@ -351,10 +348,9 @@ void ModelComponent::RemoveAttack() { } } -bool ModelComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportMsg = static_cast(msg); - if (!reportMsg.info) return false; - auto& cmptInfo = reportMsg.info->PushDebug("Model Behaviors (Mutable)"); +bool ModelComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + if (!reportInfo.info) return false; + auto& cmptInfo = reportInfo.info->PushDebug("Model Behaviors (Mutable)"); cmptInfo.PushDebug("Component ID") = GetComponentID(); cmptInfo.PushDebug("Name") = "Objects_" + std::to_string(m_Parent->GetLOT()) + "_name"; diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index 240ddc71..e821406b 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -32,9 +32,9 @@ public: void LoadBehaviors(); void Update(float deltaTime) override; - bool OnRequestUse(GameMessages::GameMsg& msg); - bool OnResetModelToDefaults(GameMessages::GameMsg& msg); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnRequestUse(GameMessages::RequestUse& requestUse); + bool OnResetModelToDefaults(GameMessages::ResetModelToDefaults& resetModelToDefaults); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index faedc848..3519712a 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -16,6 +16,7 @@ #include "CDComponentsRegistryTable.h" #include "QuickBuildComponent.h" #include "CDPhysicsComponentTable.h" +#include "Amf3.h" #include "dNavMesh.h" @@ -57,6 +58,8 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component m_SavedVelocity = NiPoint3Constant::ZERO; m_IsBounced = false; + RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo); + if (!m_Parent->GetComponent()) SetPath(m_Parent->GetVarAsString(u"attached_path")); } @@ -422,3 +425,63 @@ void MovementAIComponent::SetMaxSpeed(const float value) { m_MaxSpeed = value; m_Acceleration = value / 5; } + +bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + + auto& movementInfo = reportInfo.info->PushDebug("MovementAI"); + if (m_Path) { + movementInfo.PushDebug("Path") = m_Path->pathName; + } + + auto& movementAiInfo = movementInfo.PushDebug("Movement AI Info"); + movementAiInfo.PushDebug("Movement Type") = m_Info.movementType; + movementAiInfo.PushDebug("Wander Radius") = m_Info.wanderRadius; + movementAiInfo.PushDebug("Wander Speed") = m_Info.wanderSpeed; + movementAiInfo.PushDebug("Wander Chance") = m_Info.wanderChance; + movementAiInfo.PushDebug("Wander Delay Min") = m_Info.wanderDelayMin; + movementAiInfo.PushDebug("Wander Delay Max") = m_Info.wanderDelayMax; + + auto& speedInfo = movementInfo.PushDebug("Speed Info"); + speedInfo.PushDebug("Base Speed") = m_BaseSpeed; + speedInfo.PushDebug("Max Speed") = m_MaxSpeed; + speedInfo.PushDebug("Current Speed") = m_CurrentSpeed; + speedInfo.PushDebug("Acceleration") = m_Acceleration; + + movementInfo.PushDebug("Halt Distance") = m_HaltDistance; + movementInfo.PushDebug("Time To Travel") = m_TimeToTravel; + movementInfo.PushDebug("Time Travelled") = m_TimeTravelled; + movementInfo.PushDebug("Lock Rotation") = m_LockRotation; + movementInfo.PushDebug("Paused") = m_Paused; + movementInfo.PushDebug("Pulling To Point") = m_PullingToPoint; + + 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; + } + + i = 0; + auto& currentPath = movementInfo.PushDebug("Current Path"); + 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; + + pathCopy.pop(); + } + + return true; +} diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 72ff45e8..2a095716 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -211,6 +211,7 @@ public: bool IsPaused() const { return m_Paused; } + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); private: /** diff --git a/dGame/dComponents/PhantomPhysicsComponent.cpp b/dGame/dComponents/PhantomPhysicsComponent.cpp index 4ba95f5f..01ef7dfd 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.cpp +++ b/dGame/dComponents/PhantomPhysicsComponent.cpp @@ -29,7 +29,7 @@ #include "dpShapeSphere.h" PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &PhantomPhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&PhantomPhysicsComponent::OnGetObjectReportInfo); m_Position = m_Parent->GetDefaultPosition(); m_Rotation = m_Parent->GetDefaultRotation(); @@ -242,9 +242,8 @@ void PhantomPhysicsComponent::SetRotation(const NiQuaternion& rot) { if (m_dpEntity) m_dpEntity->SetRotation(rot); } -bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); if (!reportInfo.subCategory) { return false; } diff --git a/dGame/dComponents/PhantomPhysicsComponent.h b/dGame/dComponents/PhantomPhysicsComponent.h index ac42dca3..115783ac 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.h +++ b/dGame/dComponents/PhantomPhysicsComponent.h @@ -116,7 +116,7 @@ public: void SetMax(uint32_t max); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * A scale to apply to the size of the physics object diff --git a/dGame/dComponents/PhysicsComponent.cpp b/dGame/dComponents/PhysicsComponent.cpp index 7d301476..16722c54 100644 --- a/dGame/dComponents/PhysicsComponent.cpp +++ b/dGame/dComponents/PhysicsComponent.cpp @@ -31,11 +31,11 @@ PhysicsComponent::PhysicsComponent(Entity* parent, const int32_t componentID) : if (m_Parent->HasVar(u"CollisionGroupID")) m_CollisionGroup = m_Parent->GetVar(u"CollisionGroupID"); - RegisterMsg(MessageType::Game::GET_POSITION, this, &PhysicsComponent::OnGetPosition); + RegisterMsg(&PhysicsComponent::OnGetPosition); } -bool PhysicsComponent::OnGetPosition(GameMessages::GameMsg& msg) { - static_cast(msg).pos = GetPosition(); +bool PhysicsComponent::OnGetPosition(GameMessages::GetPosition& msg) { + msg.pos = GetPosition(); return true; } @@ -245,8 +245,7 @@ void PhysicsComponent::SpawnVertices(dpEntity* entity) const { } } -bool PhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); +bool PhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& info = reportInfo.info->PushDebug("Physics"); reportInfo.subCategory = &info; diff --git a/dGame/dComponents/PhysicsComponent.h b/dGame/dComponents/PhysicsComponent.h index 67a4a0a5..066451da 100644 --- a/dGame/dComponents/PhysicsComponent.h +++ b/dGame/dComponents/PhysicsComponent.h @@ -7,6 +7,7 @@ namespace GameMessages { struct GetObjectReportInfo; + struct GetPosition; }; namespace Raknet { @@ -33,7 +34,7 @@ public: int32_t GetCollisionGroup() const noexcept { return m_CollisionGroup; } void SetCollisionGroup(int32_t group) noexcept { m_CollisionGroup = group; } protected: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); dpEntity* CreatePhysicsEntity(eReplicaComponentType type); @@ -41,7 +42,7 @@ protected: void SpawnVertices(dpEntity* entity) const; - bool OnGetPosition(GameMessages::GameMsg& msg); + bool OnGetPosition(GameMessages::GetPosition& msg); NiPoint3 m_Position; diff --git a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp index 8f8df0ad..fde91d00 100644 --- a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp +++ b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp @@ -14,7 +14,7 @@ #include "Amf3.h" RigidbodyPhantomPhysicsComponent::RigidbodyPhantomPhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo); m_Position = m_Parent->GetDefaultPosition(); m_Rotation = m_Parent->GetDefaultRotation(); @@ -59,9 +59,8 @@ void RigidbodyPhantomPhysicsComponent::SpawnVertices() const { PhysicsComponent::SpawnVertices(m_dpEntity); } -bool RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); auto& info = reportInfo.subCategory->PushDebug("Rigidbody Phantom Info"); info.PushDebug("Scale") = m_Scale; return true; diff --git a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h index dc18da49..75cb11cd 100644 --- a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h +++ b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h @@ -29,7 +29,7 @@ public: void SpawnVertices() const; private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); float m_Scale{}; diff --git a/dGame/dComponents/ScriptComponent.cpp b/dGame/dComponents/ScriptComponent.cpp index c9123377..87b6e6e1 100644 --- a/dGame/dComponents/ScriptComponent.cpp +++ b/dGame/dComponents/ScriptComponent.cpp @@ -9,13 +9,12 @@ #include "Amf3.h" ScriptComponent::ScriptComponent(Entity* parent, const int32_t componentID, const std::string& scriptName, bool serialized, bool client) : Component(parent, componentID) { - using namespace GameMessages; m_Serialized = serialized; m_Client = client; m_ScriptName = scriptName; SetScript(scriptName); - RegisterMsg(this, &ScriptComponent::OnGetObjectReportInfo); + RegisterMsg(&ScriptComponent::OnGetObjectReportInfo); } void ScriptComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { @@ -52,10 +51,9 @@ void ScriptComponent::SetScript(const std::string& scriptName) { m_Script = CppScripts::GetScript(m_Parent, scriptName); } -bool ScriptComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& infoMsg = static_cast(msg); +bool ScriptComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { - auto& scriptInfo = infoMsg.info->PushDebug("Script"); + auto& scriptInfo = reportInfo.info->PushDebug("Script"); scriptInfo.PushDebug("Script Name") = m_ScriptName.empty() ? "None" : m_ScriptName; auto& networkSettings = scriptInfo.PushDebug("Network Settings"); for (const auto* const setting : m_Parent->GetNetworkSettings()) { diff --git a/dGame/dComponents/ScriptComponent.h b/dGame/dComponents/ScriptComponent.h index cf4b2537..e79b8e91 100644 --- a/dGame/dComponents/ScriptComponent.h +++ b/dGame/dComponents/ScriptComponent.h @@ -43,7 +43,7 @@ public: */ void SetScript(const std::string& scriptName); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); private: diff --git a/dGame/dComponents/SimplePhysicsComponent.cpp b/dGame/dComponents/SimplePhysicsComponent.cpp index c0efa544..dcb87021 100644 --- a/dGame/dComponents/SimplePhysicsComponent.cpp +++ b/dGame/dComponents/SimplePhysicsComponent.cpp @@ -16,7 +16,7 @@ #include "Amf3.h" SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &SimplePhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&SimplePhysicsComponent::OnGetObjectReportInfo); m_Position = m_Parent->GetDefaultPosition(); m_Rotation = m_Parent->GetDefaultRotation(); @@ -76,9 +76,8 @@ void SimplePhysicsComponent::SetPhysicsMotionState(uint32_t value) { m_PhysicsMotionState = value; } -bool SimplePhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +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; diff --git a/dGame/dComponents/SimplePhysicsComponent.h b/dGame/dComponents/SimplePhysicsComponent.h index d928e489..40268922 100644 --- a/dGame/dComponents/SimplePhysicsComponent.h +++ b/dGame/dComponents/SimplePhysicsComponent.h @@ -86,7 +86,7 @@ public: void SetClimbableType(const eClimbableType& value) { m_ClimbableType = value; } private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * The current velocity of the entity diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 3b27b9f1..b7558d4b 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -936,6 +936,12 @@ namespace GameMessages { LWOOBJID lootOwnerID{}; }; + struct IsDead : public GameMsg { + IsDead() : GameMsg(MessageType::Game::IS_DEAD) {} + + bool bDead{}; + }; + struct ToggleGMInvis : public GameMsg { ToggleGMInvis() : GameMsg(MessageType::Game::TOGGLE_GM_INVIS) {} @@ -955,11 +961,5 @@ namespace GameMessages { LWOOBJID childID{}; }; - - struct IsDead : public GameMsg { - IsDead() : GameMsg(MessageType::Game::IS_DEAD) {} - - bool bDead{}; - }; }; #endif // GAMEMESSAGES_H From 4ab09cf1aad216f5da5f8e91f688beb117d92038 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Wed, 27 May 2026 02:35:00 -0700 Subject: [PATCH 37/69] Remove non-functioning saving of gm invis, re-add gm invis as a feature (#1976) Does not save, only works for this world. Fixed an issue where the incorrect comparison was used to make players invisible again (the same check that makes then INvisible needs to make them visible.) --- dGame/dComponents/GhostComponent.cpp | 29 +++------------------------- dGame/dComponents/GhostComponent.h | 2 -- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index 8223080d..c27cd31a 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -29,26 +29,6 @@ GhostComponent::~GhostComponent() { } } -void GhostComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) { - auto* objElement = doc.FirstChildElement("obj"); - if (!objElement) return; - auto* ghstElement = objElement->FirstChildElement("ghst"); - if (!ghstElement) return; - m_IsGMInvisible = ghstElement->BoolAttribute("i"); -} - -void GhostComponent::UpdateXml(tinyxml2::XMLDocument& doc) { - auto* objElement = doc.FirstChildElement("obj"); - if (!objElement) return; - auto* ghstElement = objElement->FirstChildElement("ghst"); - if (ghstElement) objElement->DeleteChild(ghstElement); - // Only save if GM invisible - const auto* const user = UserManager::Instance()->GetUser(m_Parent->GetSystemAddress()); - if (!m_IsGMInvisible || !user || user->GetMaxGMLevel() < eGameMasterLevel::FORUM_MODERATOR) return; - ghstElement = objElement->InsertNewChildElement("ghst"); - if (ghstElement) ghstElement->SetAttribute("i", m_IsGMInvisible); -} - void GhostComponent::SetGhostReferencePoint(const NiPoint3& value) { m_GhostReferencePoint = value; } @@ -107,7 +87,7 @@ bool GhostComponent::OnToggleGMInvis(GameMessages::ToggleGMInvis& gmInvisMsg) { Game::entityManager->DestructEntity(m_Parent, player->GetSystemAddress()); } } else { - if (toUser->GetMaxGMLevel() >= thisUser->GetMaxGMLevel()) { + if (toUser->GetMaxGMLevel() < thisUser->GetMaxGMLevel()) { Game::entityManager->ConstructEntity(m_Parent, player->GetSystemAddress()); auto* controllableComp = m_Parent->GetComponent(); controllableComp->SetDirtyPosition(true); @@ -121,11 +101,8 @@ bool GhostComponent::OnToggleGMInvis(GameMessages::ToggleGMInvis& gmInvisMsg) { bool GhostComponent::OnGetGMInvis(GameMessages::GetGMInvis& gmInvisMsg) { LOG_DEBUG("GM Invisibility requested: %s", m_IsGMInvisible ? "true" : "false"); - // TODO: disabled for now while bugs are fixed - // gmInvisMsg.bGMInvis = m_IsGMInvisible; - // return gmInvisMsg.bGMInvis; - gmInvisMsg.bGMInvis = false; - return false; + gmInvisMsg.bGMInvis = m_IsGMInvisible; + return gmInvisMsg.bGMInvis; } bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) { diff --git a/dGame/dComponents/GhostComponent.h b/dGame/dComponents/GhostComponent.h index 4bd3318a..13a89640 100644 --- a/dGame/dComponents/GhostComponent.h +++ b/dGame/dComponents/GhostComponent.h @@ -16,8 +16,6 @@ public: static inline const eReplicaComponentType ComponentType = eReplicaComponentType::GHOST; GhostComponent(Entity* parent, const int32_t componentID); ~GhostComponent() override; - void LoadFromXml(const tinyxml2::XMLDocument& doc) override; - void UpdateXml(tinyxml2::XMLDocument& doc) override; void SetGhostOverride(bool value) { m_GhostOverride = value; }; From 4d6a624da2cc35b805aa328c9c81cf0e3592c0fc Mon Sep 17 00:00:00 2001 From: Johntor Date: Tue, 2 Jun 2026 10:00:13 +0300 Subject: [PATCH 38/69] docs: Refine command documentation (#1978) * Refine command documentation Updated command descriptions From dGame/dUtilities/SlashCommandHandler.cpp. Added aliases and improved formatting for better readability. * fix: grammatical feedback --------- Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> --- docs/Commands.md | 238 ++++++++++++++++++++++++++++++----------------- 1 file changed, 152 insertions(+), 86 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index f9a00452..702f64cc 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -1,51 +1,115 @@ - # In-game commands -* All commands are prefixed by `/` and typed in the in-game chat window. Some commands require elevated gmlevel privileges. Operands within `<>` are required, operands within `()` are not. + +* All commands are prefixed by `/` and typed in the in-game chat window. Some commands require elevated gmlevel privileges. Operands within `<>` are required, operands within `()` are optional. ## General Commands |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | +|help|`/help (command/page)`|If a command is given, display detailed info on that command. Otherwise display a list of commands with short descriptions. Aliases: `/h`.|0| |credits|`/credits`|Displays the names of the people behind Darkflame Universe.|0| -|die|`/die`|Smashes the player.|0| -|info|`/info`|Displays server info to the user, including where to find the server's source code.|0| -|instanceinfo|`/instanceinfo`|Displays in the chat the current zone, clone, and instance id.|0| -|ping|`/ping (-l)`|Displays in chat your average ping. If the `-l` flag is used, the latest ping is displayed.|| -|pvp|`/pvp`|Toggle your PVP flag.|0| -|resurrect|`/resurrect`|Resurrects the player.|0| -|requestmailcount|`/requestmailcount`|Sends notification with number of unread messages in the player's mailbox.|0| -|who|`/who`|Displays in chat all players on the instance.|0| -|togglenameplate|`/togglenameplate`|Turns the nameplate above your head that is visible to other players off and on.|8 unless `allow_nameplate_off` is set to exactly `1` in the settings then admin level requirement is 0| -|toggleskipcinematics|`/toggleskipcinematics`|Skips mission and world load related cinematics.|8 unless `allow_players_to_skip_cinematics` is set to exactly `1` in the settings then admin level requirement is 0| +|die|`/die`|Smashes the player as if they were killed by something|0| +|info|`/info`|Displays server info to the user, including where to find the server's source code|0| +|instanceinfo|`/instanceinfo`|Display LWOZoneID info for the current zone|0| +|ping|`/ping (-l)`|Displays your average ping. If the `-l` flag is used, the latest ping is displayed.|0| +|pvp|`/pvp`|Toggle your PVP flag|0| +|requestmailcount|`/requestmailcount`|Sends notification with number of unread messages in the player's mailbox. Aliases: `/checkmail`.|0| +|who|`/who`|Displays all players on the instance|0| +|togglenameplate|`/togglenameplate`|Turns the nameplate above your head that is visible to other players off and on. This must be enabled by a server admin. Aliases: `/tnp`.|8 or 0 if `allow_nameplate_off` is 1| +|toggleskipcinematics|`/toggleskipcinematics`|Skips mission and world load related cinematics. This must be enabled by a server admin. Aliases: `/tsc`.|8 or 0 if `allow_players_to_skip_cinematics` is 1| + +## Client-handled Commands + +These commands are registered by the server so they appear in help, but their behavior is handled by the LEGO Universe client. + +### User-Client related +|Command|Usage|Description|Admin Level Requirement| +|--- |--- |--- |--- | +|faq|`/faq`|Show the LU FAQ Page. Aliases: `/faqs`.|0| +|camp|`/camp`|Returns you to the character select screen. Aliases: `/logoutcharacter`.|0| +|cancelqueue|`/cancelqueue`|Cancel Your position in the queue if you are in one.|0| +|exit|`/exit`|Exit to desktop. Aliases: `/quit`.|0| +|forums|`/forums`|Show the LU Forums!|0| +|loc|`/loc`|Output your current location on the map to the chat box. Aliases: `/locate`, `/location`.|0| +|logout|`/logout`|Returns you to the login screen. Aliases: `/logoutaccount`.|0| +|minigames|`/minigames`|Show the LEGO minigames page!|0| +|shop|`/shop`|Show the LEGO shop page. Aliases: `/store`.|0| +|perfoptionslow|`/perfoptionslow`|Sets the default low-spec performance options in the cfg file|0| +|perfoptionsmid|`/perfoptionsmid`|Sets the default medium-spec performance options in the cfg file|0| +|perfoptionshigh|`/perfoptionshigh`|Sets the default high-spec performance options in the cfg file|0| +|recommendedperfoptions|`/recommendedperfoptions`|Sets the recommended performance options in the cfg file|0| + + +### Friend-Team related +|Command|Usage|Description|Admin Level Requirement| +|--- |--- |--- |--- | +|addfriend|`/addfriend `|[name] Add a player to your friends list.|0| +|addignore|`/addignore `|[name] Add a player to your ignore list.|0| +|invite|`/invite `|[name] Invite a player to your team. Aliases: `/inviteteam`, `/teaminvite`, `/tinvite`.|0| +|kickplayer|`/kickplayer `|Kicks a player from your current team. The `/kick` alias is reserved by the moderation command. Aliases: `/teamkickplayer`, `/tkick`, `/tkickplayer`.|0| +|leader|`/leader `|[name] Set the leader for your current team. Aliases: `/setleader`, `/teamsetleader`, `/tleader`, `/tsetleader`.|0| +|leave|`/leave`|Leave your current team. Aliases: `/leaveteam`, `/teamleave`, `/tleave`.|0| +|removefriend|`/removefriend `|[name] Removes a player from your friends list.|0| +|removeIgnore|`/removeIgnore `|[name] Removes a player from your ignore list.|0| +|setloot|`/setloot `|[rr\|ffa] Set the loot for your current team (round-robin/free for all). Aliases: `/teamsetloot`, `/tloot`, `/tsetloot`.|0| +|s|`/s `|Say something outloud so that everyone can hear you. Aliases: `/say`.|0| +|team|`/team `|Send a message to your teammates. Aliases: `/t`.|0| +|tell|`/tell `|Send a private message to another player. Aliases: `/w`, `/whisper`.|0| + + +### Actions +|Command|Usage|Description|Admin Level Requirement| +|--- |--- |--- |--- | +|yes|`/yes`|Aye aye, captain!|0| +|sigh|`/sigh`|Another day, another brick.|0| +|shrug|`/shrug`|I dunno...|0| +|talk|`/talk`|Jibber Jabber|0| +|thumb|`/thumb`|Oh, yeah!. Aliases: `/thumbs`, `/thumbsup`.|0| +|cringe|`/cringe`|I don't even want to talk about it...|0| +|victory!|`/victory!`|Victory!|0| +|backflip|`/backflip`|Do a flip!|0| +|clap|`/clap`|A round of applause!|0| +|thanks|`/thanks`|Express your gratitude for another.|0| +|wave|`/wave`|Wave to other players.|0| +|why|`/why`|Why\|!?!!|0| +|gasp|`/gasp`|Oh my goodness!|0| +|cry|`/cry`|Show everyone your 'Aw' face.|0| +|dance|`/dance`|Dance 'til you can't dance no more.|0| +|giggle|`/giggle`|A good little chuckle|0| +|salute|`/salute`|For those about to build...|0| ## Moderation Commands |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | -|gmlevel|`/gmlevel `|Within the authorized range of levels for the current account, changes the character's game master level to the specified value. This is required to use certain commands. Aliases: `/setgmlevel`, `/makegm`.|Account GM level greater than 0| -|kick|`/kick `|Kicks the player off the server.|2| -|mailitem|`/mailitem `|Mails an item to the given player. The mailed item has predetermined content. The sender name is set to "Darkflame Universe." The title of the message is "Lost item." The body of the message is "This is a replacement item for one you lost."|3| -|ban|`/ban `|Bans a user from the server.|4| -|approveproperty|`/approveproperty`|Approves the property the player is currently visiting.|5| +|gmlevel|`/gmlevel `|Within the authorized range of levels for the current account, changes the character's game master level to the specified value. This is required to use certain commands. Aliases: `/makegm`, `/setgmlevel`.|0; account GM level must be greater than 0| +|kick|`/kick `|Kicks the player off the server|2| +|mailitem|`/mailitem `|Mails an item to the given player. The mailed item has predetermined content. The sender name is set to "Darkflame Universe". The title of the message is "Lost item". The body of the message is "This is a replacement item for one you lost".|3| +|ban|`/ban `|Bans a user from the server|4| +|approveproperty|`/approveproperty`|Approves the property the player is currently visiting|5| |mute|`/mute (days) (hours)`|Mute player for the given amount of time. If no time is given, the mute is indefinite.|6| -|fly|`/fly `|This toggles your flying state with an optional parameter for the speed scale.|6| -|attackimmune|`/attackimmune `|Sets the character's immunity to basic attacks state, where value can be one of "1", to make yourself immune to basic attack damage, or "0" to undo.|8| -|gmimmune|`/gmimmunve `|Sets the character's GMImmune state, where value can be one of "1", to make yourself immune to damage, or "0" to undo.|8| -|gminvis|`/gminvis`|Toggles invisibility for the character, though it's currently a bit buggy. Requires nonzero GM Level for the character, but the account must have a GM level of 8.|8| -|setname|`/setname `|Sets a temporary name for your player. The name resets when you log out.|8| -|title|`/title `|Temporarily appends your player's name with " - <title>". This resets when you log out.|8| +|fly|`/fly <speed>`|Toggles your flying state with an optional parameter for the speed scale.|8| +|attackimmune|`/attackimmune <value>`|Sets the character's immunity to basic attacks state, where value can be one of "1", to make yourself immune to basic attack damage, or "0" to undo|8| +|gmimmune|`/gmimmune <value>`|Sets the character's GMImmune state, where value can be one of "1", to make yourself immune to damage, or "0" to undo|8| +|gminvis|`/gminvis`|Toggles invisibility for the character. Does not save across worlds.|1| +|setname|`/setname <name>`|Sets a temporary name for your player. The name resets when you log out|8| +|title|`/title <title>`|Temporarily appends your player's name with " - <title>". This resets when you log out|8| +|showall|`/showall (displayZoneData) (displayIndividualPlayers)`|Usage: /showall (displayZoneData: Default 1) (displayIndividualPlayers: Default 1)|2| +|findplayer|`/findplayer <player name>`|Find the World Server a player is in if they are online|2| +|spectate|`/spectate (player name)`|Specify a player name to spectate. They must be in the same world as you. Leave blank to stop spectating. Aliases: `/follow`.|2| ## Server Operation Commands |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | -|announce|`/announce`|Sends a announcement. `/setanntitle` and `/setannmsg` must be called first to configure the announcement.|8| -|kill|`/kill <username>`|Smashes the character whom the given user is playing.|8| -|metrics|`/metrics`|Prints some information about the server's performance.|8| -|setannmsg|`/setannmsg <title>`|Sets the message of an announcement.|8| -|setanntitle|`/setanntitle <title>`|Sets the title of an announcement.|8| +|announce|`/announce`|Sends an announcement. `/setanntitle` and `/setannmsg` must be called first to configure the announcement.|8| +|kill|`/kill <username>`|Smashes the character whom the given user is playing|8| +|metrics|`/metrics`|Prints some information about the server's performance|8| +|setannmsg|`/setannmsg <message>`|Sets the message of an announcement. Use with `/setanntitle` and `/announce`|8| +|setanntitle|`/setanntitle <title>`|Sets the title of an announcement. Use with `/setannmsg` and `/announce`|8| +|shutdown|`/shutdown`|Shuts this world down|8| |shutdownuniverse|`/shutdownuniverse`|Sends a shutdown message to the master server. This will send an announcement to all players that the universe will shut down in 10 minutes.|9| -|uptime|`/uptime`|Displays the time the current world server has been active.|8| +|uptime|`/uptime`|Display the time the current world server has been active|8| ## Development Commands @@ -53,73 +117,75 @@ These commands are primarily for development and testing. The usage of many of t |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | -|fix-stats|`/fix-stats`|Resets skills, buffs, and destroyables.|0| -|join|`/join <password>`|Joins a private zone with given password.|0| -|leave-zone|`/leave-zone` or <br> `/leavezone`|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.|0| +|fix-stats|`/fix-stats`|Resets skills, buffs, and destroyables|0| +|join|`/join <password>`|Join a private zone with given password|0| +|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 <body part> <minifig item id>`|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 <zone> (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).|1| -|reportproxphys|`/reportproxphys`|Prints to console the position and radius of proximity sensors.|6| -|spawnphysicsverts|`/spawnphysicsverts`|Spawns a 1x1 brick at all vertices of phantom physics objects.|6| -|teleport|`/teleport <x/source player> (y) <z/target player>` or <br> `/tele <x/source player> (y) <z/target player>`|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. Alias: `/tele`.|6| -|activatespawner|`/activatespawner <spawner name>`|Activates spawner by name.|8| +|testmap|`/testmap <zone> (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| +|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 <x/source player> (y) <z/target player>`|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| +|activatespawner|`/activatespawner <spawner name>`|Activates spawner by name|8| |addmission|`/addmission <mission id>`|Accepts the mission, adding it to your journal.|8| |boost|`/boost (time)`|Adds a passive boost action if you are in a vehicle. If time is given it will end after that amount of time|8| |unboost|`/unboost`|Removes a passive vehicle boost|8| -|buff|`/buff <id> <duration>`|Applies the buff with the given id for the given number of seconds.|8| -|buffme|`/buffme`|Sets health, armor, and imagination to 999.|8| -|buffmed|`/buffmed`|Sets health, armor, and imagination to 9.|8| -|clearflag|`/clearflag <flag id>`|Removes the given health or inventory flag from your player. Equivalent of calling `/setflag off <flag id>`.|8| -|completemission|`/completemission <mission id>`|Completes the mission, removing it from your journal.|8| -|createprivate|`/createprivate <zone id> <clone id> <password>`|Creates a private zone with password.|8| -|debugui|`/debugui`|Toggle Debug UI.|8| -|dismount|`/dismount`|Dismounts you from the vehicle or mount.|8| -|reloadconfig|`/reloadconfig`|Reloads the server with the new config values.|8| -|force-save|`/force-save`|While saving to database usually happens on regular intervals and when you disconnect from the server, this command saves your player's data to the database.|8| -|freecam|`/freecam`|Toggles freecam mode.|8| -|freemoney|`/freemoney <coins>`|Gives coins.|8| -|getnavmeshheight|`/getnavmeshheight`|Displays the navmesh height at your current position.|8| -|giveuscore|`/giveuscore <uscore>`|Gives uscore.|8| -|gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id.|8| -|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> \| -a <animation> \| -s \| -p \| -f (faction) \| -t)`|Finds the closest entity with the given component or LDF 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 `/inspect` Usage](#detailed-inspect-usage) below.|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.|8| -|locrow|`/locrow`|Prints the your current position and rotation information to the console.|8| -|lookup|`/lookup <query>`|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8| -|playanimation|`/playanimation <id>`|Plays animation with given ID. Alias: `/playanim`.|8| -|playeffect|`/playeffect <effect id> <effect type> <effect name>`|Plays an effect.|8| -|playlvlfx|`/playlvlfx`|Plays the level up animation on your character.|8| -|playrebuildfx|`/playrebuildfx`|Plays the quickbuild animation on your character.|8| -|pos|`/pos`|Displays your current position in chat and in the console.|8| -|refillstats|`/refillstats`|Refills health, armor, and imagination to their maximum level.|8| -|reforge|`/reforge <base item id> <reforged item id>`|Reforges an item.|8| -|resetmission|`/resetmission <mission id>`|Sets the state of the mission to accepted but not yet started.|8| -|rot|`/rot`|Displays your current rotation in chat and in the console.|8| +|buff|`/buff <id> <duration>`|Applies a buff with the given id for the given number of seconds|8| +|buffme|`/buffme`|Sets health, armor, and imagination to 999|8| +|buffmed|`/buffmed`|Sets health, armor, and imagination to 9|8| +|clearflag|`/clearflag <flag id>`|Removes the given health or inventory flag from your player. Equivalent of calling `/setflag off <flag id>`|8| +|completemission|`/completemission <mission id>`|Completes the mission, removing it from your journal|8| +|createprivate|`/createprivate <zone id> <clone id> <password>`|Creates a private zone with password|8| +|debugui|`/debugui`|Toggle Debug UI|8| +|dismount|`/dismount`|Dismounts you from the vehicle or mount|8| +|reloadconfig|`/reloadconfig`|Reloads the server with the new config values. Aliases: `/reload-config`.|8| +|forcesave|`/forcesave`|While saving to database usually happens on regular intervals and when you disconnect from the server, this command saves your player's data to the database. Aliases: `/force-save`.|8| +|freecam|`/freecam`|Toggles freecam mode|8| +|freemoney|`/freemoney <coins>`|Give yourself coins. Aliases: `/givemoney`, `/money`, `/givecoins`, `/coins`.|8| +|getnavmeshheight|`/getnavmeshheight`|Display the navmesh height at your current position|8| +|giveuscore|`/giveuscore <uscore>`|Gives uscore|8| +|gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id. Aliases: `/give`.|8| +|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> | -a <animation> | -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| +|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| +|lookup|`/lookup <query>`|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8| +|playanimation|`/playanimation <id>`|Play an animation with given ID. Aliases: `/playanim`.|8| +|playeffect|`/playeffect <effect id> <effect type> <effect name>`|Plays an effect|8| +|playlvlfx|`/playlvlfx`|Plays the level up animation on your character|8| +|playrebuildfx|`/playrebuildfx`|Plays the quickbuild animation on your character|8| +|pos|`/pos`|Displays your current position in chat and in the console|8| +|refillstats|`/refillstats`|Refills health, armor, and imagination to their maximum level|8| +|reforge|`/reforge <base item id> <reforged item id>`|Reforges an item|8| +|resetmission|`/resetmission <mission id>`|Sets the state of the mission to accepted but not yet started|8| +|rot|`/rot`|Displays your current rotation in chat and in the console|8| |runmacro|`/runmacro <macro>`|Runs any command macro found in `./res/macros/`|8| -|setcontrolscheme|`/setcontrolscheme <scheme number>`|Sets the character control scheme to the specified number.|8| -|setcurrency|`/setcurrency <coins>`|Sets your coins.|8| -|setflag|`/setflag (value) <flag id>`|Sets the given inventory or health flag to the given value, where value can be one of "on" or "off". If no value is given, by default this adds the flag to your character (equivalent of calling `/setflag on <flag id>`).|8| -|setinventorysize|`/setinventorysize <size> (inventory)` or <br> `/setinvsize <size> (inventory)`|Sets your inventory size to the given size. If `inventory` is provided, the number or string will be used to set that inventory to the requested size. Alias: `/setinvsize`|8| -|setuistate|`/setuistate <ui state>`|Changes UI state.|8| -|spawn|`/spawn <id>`|Spawns an object at your location by id.|8| +|setcontrolscheme|`/setcontrolscheme <scheme number>`|Sets the character control scheme to the specified number|8| +|setcurrency|`/setcurrency <coins>`|Sets your coins. Aliases: `/setcoins`.|8| +|setflag|`/setflag (value) <flag id>`|Sets the given inventory or health flag to the given value, where value can be one of "on" or "off". If no value is given, by default this adds the flag to your character (equivalent of calling `/setflag on <flag id>`)|8| +|setinventorysize|`/setinventorysize <size> (inventory)`|Sets your inventory size to the given size. If `inventory` is provided, the number or string will be used to set that inventory to the requested size. Aliases: `/setinvsize`, `/setinvensize`.|8| +|setuistate|`/setuistate <ui state>`|Changes UI state|8| +|spawn|`/spawn <id>`|Spawns an object at your location by id|8| |spawngroup|`/spawngroup <id> <amount> <radius>`|Spawns `<amount>` of object `<id>` within the given `<radius>` from your location|8| -|speedboost|`/speedboost <amount>`|Sets the speed multiplier to the given amount. `/speedboost 1.5` will set the speed multiplier to 1.5x the normal speed.|8| -|startcelebration|`/startcelebration <id>`|Starts a celebration effect on your character.|8| -|stopeffect|`/stopeffect <effect id>`|Stops the given effect.|8| -|toggle|`/toggle <ui state>`|Toggles UI state.|8| -|tpall|`/tpall`|Teleports all characters to your current position.|8| -|triggerspawner|`/triggerspawner <spawner name>`|Triggers spawner by name.|8| -|unlock-emote|`/unlock-emote <emote id>`|Unlocks for your character the emote of the given id.|8| -|Set Level|`/setlevel <requested_level> (username)`|Sets the using entities level to the requested level. Takes an optional parameter of an in-game players username to set the level of.|8| -|setskillslot|`/setskillslot <slot> <skill id>`||8| +|speedboost|`/speedboost <amount>`|Sets the speed multiplier to the given amount. `/speedboost 1.5` will set the speed multiplier to 1.5x the normal speed|8| +|startcelebration|`/startcelebration <id>`|Starts a celebration effect on your character|8| +|stopeffect|`/stopeffect <effect id>`|Stops the given effect|8| +|toggle|`/toggle <ui state>`|Toggles UI state|8| +|tpall|`/tpall`|Teleports all characters to your current position|8| +|triggerspawner|`/triggerspawner <spawner name>`|Triggers spawner by name|8| +|unlock-emote|`/unlock-emote <emote id>`|Unlocks for your character the emote of the given id. Aliases: `/unlockemote`.|8| +|setlevel|`/setlevel <requested_level> (username)`|Sets the using entities level to the requested level. Takes an optional parameter of an in-game players username to set the level of|8| +|setskillslot|`/setskillslot <slot> <skill id>`|Set an action slot to a specific skill|8| |setfaction|`/setfaction <faction id>`|Clears the users current factions and sets it|8| |addfaction|`/addfaction <faction id>`|Add the faction to the users list of factions|8| |getfactions|`/getfactions`|Shows the player's factions|8| |setrewardcode|`/setrewardcode <code>`|Sets the rewardcode for the account you are logged into if it's a valid rewardcode, See cdclient table `RewardCodes`|8| -|barfight|`/barfight start`|Starts a barfight (turns everyones pvp on)|8| -|despawn|`/despawn <objectID>`|Despawns the entity objectID IF it was spawned in through a slash command.|8| -|execute|`/execute <subcommand> ... run <command>`|Execute commands with modified context (Minecraft-style). Subcommands: `as <playername>` (execute as different player), `at <playername>` (execute from player's position), `positioned <x> <y> <z>` (execute from coordinates - supports absolute coordinates like `100 200 300` or relative coordinates like `~5 ~10 ~` where `~` means current position). Example: `/execute as Player1 run pos`, `/execute positioned ~5 ~ ~-3 run spawn 1234`|8| -|crash|`/crash`|Crashes the server.|9| -|rollloot|`/rollloot <loot matrix index> <item id> <amount>`|Given a `loot matrix index`, look for `item id` in that matrix `amount` times and print to the chat box statistics of rolling that loot matrix.|9| -|castskill|`/castskill <skill id>`|Casts the skill as the player|9| +|barfight|`/barfight`|Starts a barfight (turns everyones pvp on)|8| +|despawn|`/despawn <object id>`|Despawns an object by id|8| +|execute|`/execute <subcommand> ... run <command>`|Execute commands as different entities or from different positions. Usage: /execute <subcommand> ... run <command>. Subcommands: as <entity>, at <entity>, positioned <x> <y> <z>. Aliases: `/exec`.|8| +|crash|`/crash`|Crashes the server. Aliases: `/pumpkin`.|9| +|rollloot|`/rollloot <loot matrix index> <item id> <amount>`|Given a `loot matrix index`, look for `item id` in that matrix `amount` times and print to the chat box statistics of rolling that loot matrix. Aliases: `/roll-loot`.|8| +|castskill|`/castskill <skill id>`|Casts the skill as the player|8| +|deleteinven|`/deleteinven <inventory>`|Delete all items from a specified inventory|8| ## Detailed `/inspect` Usage From 0c1808686cb84d50e0e7e10a63dc9d51d7650d32 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:17:00 -0700 Subject: [PATCH 39/69] fix: tac arc absolute value bug (#1979) Tested that tac arc correctly checks the full range --- dGame/dBehaviors/TacArcBehavior.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dGame/dBehaviors/TacArcBehavior.cpp b/dGame/dBehaviors/TacArcBehavior.cpp index 616651f5..bea246aa 100644 --- a/dGame/dBehaviors/TacArcBehavior.cpp +++ b/dGame/dBehaviors/TacArcBehavior.cpp @@ -208,8 +208,8 @@ void TacArcBehavior::Load() { GetFloat("offset_z", 0.0f) ); this->m_method = GetInt("method", 1); - this->m_upperBound = std::abs(GetFloat("upper_bound", 4.4f)); - this->m_lowerBound = std::abs(GetFloat("lower_bound", 0.4f)) - 5.0f; // Makes it so players and objects can still be targetted when slightly below the caster. FIXME: use bounding spheres at some point + this->m_upperBound = GetFloat("upper_bound", 4.4f); + this->m_lowerBound = GetFloat("lower_bound", 0.4f) - 5.0f; // Makes it so players and objects can still be targetted when slightly below the caster. FIXME: use bounding spheres at some point this->m_usePickedTarget = GetBoolean("use_picked_target", false); this->m_useTargetPostion = GetBoolean("use_target_position", false); this->m_checkEnv = GetBoolean("check_env", false); From 8e09ffd6e8f694c8e11d048610f96e2a032309fe Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell <aronwk.aaron@gmail.com> Date: Sat, 6 Jun 2026 01:30:12 -0500 Subject: [PATCH 40/69] feat: enhance CI/CD workflows with Docker support and artifact management (#1977) * feat: enhance CI/CD workflows with Docker support and artifact management * chore: update GitHub Actions workflows with version pinning and permission adjustments * feat: update CI workflows to support new OS versions and improve artifact handling * chore: remove macOS-specific installation of libssl and XCode switching * fix: update artifact zipping logic to target build directories in canary and release workflows * chore: update actions/checkout to version 6.0.2 in CI workflow * fix: update continue-on-error condition in CI workflow and add macOS RelWithDebInfo build preset * fix: rename step to accurately reflect Docker image signing process * don't ignore pdb's --- .github/workflows/build-and-push-docker.yml | 33 ++++++-- .github/workflows/build-and-test.yml | 74 ++++++++++++---- .github/workflows/canary.yml | 93 +++++++++++++++++++++ .github/workflows/pr-title-check.yml | 37 ++++++++ .github/workflows/release.yml | 70 ++++++++++++++++ .gitignore | 2 + CMakePresets.json | 13 ++- cliff.toml | 35 ++++++++ 8 files changed, 329 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/canary.yml create mode 100644 .github/workflows/pr-title-check.yml create mode 100644 .github/workflows/release.yml create mode 100644 cliff.toml diff --git a/.github/workflows/build-and-push-docker.yml b/.github/workflows/build-and-push-docker.yml index e4bffabe..b7ae1d06 100644 --- a/.github/workflows/build-and-push-docker.yml +++ b/.github/workflows/build-and-push-docker.yml @@ -1,14 +1,14 @@ -name: CI +name: Docker on: push: branches: - - "main" + - main tags: - "v*.*.*" pull_request: branches: - - "main" + - main env: REGISTRY: ghcr.io @@ -20,15 +20,21 @@ jobs: permissions: contents: read packages: write + attestations: write + id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: recursive + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - name: Log in to the Container registry - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -36,21 +42,32 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # generate Docker tags based on the following events/attributes tags: | type=ref,event=pr type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=canary,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - name: Build and push Docker image - uses: docker/build-push-action@v5 + id: push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Sign Docker image + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f0806156..c26f0dee 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -3,6 +3,8 @@ name: CI on: push: branches: [ main ] + tags: + - "v*.*.*" pull_request: branches: [ main ] @@ -10,38 +12,51 @@ jobs: build-and-test: name: Build & Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} - continue-on-error: true + continue-on-error: ${{ github.event_name == 'pull_request' }} strategy: matrix: - os: [ windows-2022, ubuntu-22.04, macos-15-intel ] + include: + - os: windows-2025 + artifact: windows + debug_preset: windows-msvc-relwithdebinfo + - os: ubuntu-24.04 + artifact: linux + debug_preset: linux-gnu-relwithdebinfo + - os: macos-15-intel + artifact: macos + debug_preset: macos-relwithdebinfo steps: - - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Add msbuild to PATH (Windows only) - if: ${{ matrix.os == 'windows-2022' }} - uses: microsoft/setup-msbuild@767f00a3f09872d96a0cb9fcd5e6a4ff33311330 + if: ${{ matrix.os == 'windows-2025' }} + uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3 with: vs-version: '[17,18)' msbuild-architecture: x64 - - name: Install libssl and switch to XCode 15.2 (Mac Only) - if: ${{ matrix.os == 'macos-13' }} - run: | - brew install openssl@3 - sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - name: Get CMake 3.x - uses: lukka/get-cmake@28983e0d3955dba2bb0a6810caae0c6cf268ec0c + uses: lukka/get-cmake@591817e96fcad43505fb4eae36172462abb3a42e # v4.3.3 with: - cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version + cmakeVersion: "~3.25.0" - name: cmake - uses: lukka/run-cmake@67c73a83a46f86c4e0b96b741ac37ff495478c38 + uses: lukka/run-cmake@5d55ea7949e25f69f0ecb516d8d572297e03a956 # v10.9 with: - workflowPreset: "ci-${{matrix.os}}" + workflowPreset: "${{ matrix.debug_preset }}" + + - name: Extract Linux debug symbols + if: matrix.os == 'ubuntu-24.04' + run: | + find build -type f -name '*Server' | while read bin; do + objcopy --only-keep-debug "$bin" "${bin}.debug" + objcopy --strip-debug --add-gnu-debuglink="${bin}.debug" "$bin" + done + - name: artifacts - uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: build-${{matrix.os}} + name: build-${{matrix.artifact}} path: | build/*/*Server* build/*/*.ini @@ -52,5 +67,30 @@ jobs: build/*/navmeshes/ build/*/migrations/ build/*/*.dcf - !build/*/*.pdb !build/*/d*/ + !build/*/*.dSYM/ + !build/**/*.debug + + - name: debug symbols (Windows) + if: matrix.os == 'windows-2025' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: debug-${{matrix.artifact}} + path: | + build/*/*.pdb + build/*/d*/ + retention-days: 30 + - name: debug symbols (Linux) + if: matrix.os == 'ubuntu-24.04' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: debug-${{matrix.artifact}} + path: build/**/*.debug + retention-days: 30 + - name: debug symbols (macOS) + if: matrix.os == 'macos-15-intel' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: debug-${{matrix.artifact}} + path: build/**/*.dSYM/ + retention-days: 30 diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 00000000..2a3a470b --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,93 @@ +name: Canary + +on: + workflow_run: + workflows: ["CI"] + branches: [main] + types: [completed] + +permissions: + contents: write + actions: read + +jobs: + canary-release: + name: Publish Canary Release + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Get last release tag + id: last_tag + run: | + tag=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "none") + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Generate changelog since last release tag + uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + id: cliff + with: + config: cliff.toml + args: --unreleased --strip header + env: + OUTPUT: CHANGES.md + GITHUB_REPO: ${{ github.repository }} + + - name: Prepend header to changelog + run: | + last="${{ steps.last_tag.outputs.tag }}" + sha="${{ github.event.workflow_run.head_sha }}" + short="${sha:0:7}" + if [ "$last" != "none" ]; then + header="Changes since **$last** ([full diff](https://github.com/${{ github.repository }}/compare/${last}...${sha}))\n\n" + else + header="Changes up to \`${short}\`\n\n" + fi + printf "%b" "$header" | cat - CHANGES.md > CHANGES.tmp && mv CHANGES.tmp CHANGES.md + + - name: Download artifacts from CI run + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts/ + + - name: Package artifacts + run: | + declare -A platform_map=( + ["build-windows"]="darkflame-universe-windows" + ["build-linux"]="darkflame-universe-linux" + ["build-macos"]="darkflame-universe-macos" + ) + cd artifacts + for dir in build-*/; do + name="${dir%/}" + out="${platform_map[$name]:-$name}" + zip -r "../${out}.zip" "$dir" + done + cd .. + ls -lh *.zip + + - name: Delete existing canary release + run: gh release delete canary --yes --cleanup-tag 2>/dev/null || true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create canary pre-release + uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 + with: + tag: canary + name: "Canary ${{ steps.last_tag.outputs.tag }}+${{ github.event.workflow_run.head_sha }}" + bodyFile: CHANGES.md + artifacts: "*.zip" + artifactContentType: application/zip + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: true + draft: false + allowUpdates: true + removeArtifacts: true + commit: ${{ github.event.workflow_run.head_sha }} diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 00000000..00ddf4ca --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,37 @@ +name: PR Title Check + +on: + pull_request_target: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: write + +jobs: + check-title: + name: Conventional Commit Title + runs-on: ubuntu-latest + + steps: + - name: Check PR title follows Conventional Commits + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + refactor + docs + chore + test + ci + revert + requireScope: false + subjectPattern: ^.+$ + subjectPatternError: | + The PR title "{title}" must have a description after the type/scope prefix. + Example: "feat: add new login flow" or "fix(auth): handle null token" + wip: true + validateSingleCommit: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b634b90b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +permissions: + contents: write + actions: read + +jobs: + release: + name: Create Release + # Only run when CI completed successfully on a tag push (not a PR branch named like a version) + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_branch, 'v') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Generate changelog + uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + id: cliff + with: + config: cliff.toml + args: --latest --strip header + env: + OUTPUT: CHANGES.md + GITHUB_REPO: ${{ github.repository }} + + - name: Download artifacts from CI run + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts/ + + - name: Package artifacts + run: | + declare -A platform_map=( + ["build-windows"]="darkflame-universe-windows" + ["build-linux"]="darkflame-universe-linux" + ["build-macos"]="darkflame-universe-macos" + ) + cd artifacts + for dir in build-*/; do + name="${dir%/}" + out="${platform_map[$name]:-$name}" + zip -r "../${out}.zip" "$dir" + done + cd .. + ls -lh *.zip + + - name: Create GitHub Release + uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 + with: + tag: ${{ github.event.workflow_run.head_branch }} + name: ${{ github.event.workflow_run.head_branch }} + bodyFile: CHANGES.md + artifacts: "*.zip" + artifactContentType: application/zip + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false + draft: false diff --git a/.gitignore b/.gitignore index d7af5d1f..b1a336af 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,5 @@ docker-compose.override.yml # CMake scripts !cmake/* !cmake/toolchains/* +.mcp.json +.claude/ diff --git a/CMakePresets.json b/CMakePresets.json index 9806a3a3..2819f320 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -162,6 +162,13 @@ "rhs": "Darwin" }, "binaryDir": "${sourceDir}/build/macos" + }, + { + "name": "macos-relwithdebinfo", + "inherits": ["macos", "relwithdebinfo-config"], + "displayName": "[RelWithDebInfo] MacOS", + "description": "Create a RelWithDebInfo build for MacOS", + "binaryDir": "${sourceDir}/build/macos-relwithdebinfo" } ], "buildPresets": [ @@ -255,7 +262,7 @@ { "name": "macos-relwithdebinfo", "inherits": "default", - "configurePreset": "macos", + "configurePreset": "macos-relwithdebinfo", "displayName": "[RelWithDebInfo] MacOS", "description": "This preset is used to build in release mode with debug info on MacOS", "configuration": "RelWithDebInfo" @@ -374,7 +381,7 @@ { "name": "macos-relwithdebinfo", "inherits": "default", - "configurePreset": "macos", + "configurePreset": "macos-relwithdebinfo", "displayName": "[RelWithDebInfo] MacOS", "description": "Runs all tests on a MacOS configuration", "configuration": "RelWithDebInfo" @@ -603,7 +610,7 @@ "steps": [ { "type": "configure", - "name": "macos" + "name": "macos-relwithdebinfo" }, { "type": "build", diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..18b2597e --- /dev/null +++ b/cliff.toml @@ -0,0 +1,35 @@ +[changelog] +header = "" +body = """ +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} +{% for commit in commits %} +- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/commit/{{ commit.id }}))\ +{% if commit.github.username %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){% endif %} +{% endfor %} +{% endfor %} +""" +footer = "" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^docs", group = "Documentation" }, + { message = "^chore", group = "Chores" }, + { message = "^test", group = "Testing" }, + { message = "^ci", group = "CI/CD" }, + { message = "^revert", group = "Reverts" }, +] +filter_commits = false +tag_pattern = "v[0-9].*" +skip_tags = "canary" +ignore_tags = "" +topo_order = false +sort_commits = "oldest" From f6c9a27a2b39b0b7bd2f9147a29aa239eec5bf7f Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell <aronwk.aaron@gmail.com> Date: Sun, 7 Jun 2026 04:07:17 -0500 Subject: [PATCH 41/69] fix: update permissions for PR title check workflow (#1981) --- .github/workflows/pr-title-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 00ddf4ca..a91b0ee5 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -6,6 +6,7 @@ on: permissions: pull-requests: write + statuses: write jobs: check-title: From a156a8fcbad51f10d835aa903d8e0bafcaccdbdb Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:59:11 -0700 Subject: [PATCH 42/69] fix: security vulnerabilities (#1980) * fix: security vulnerabilities Tested that all functions related to the touched files work will test sqlite on a CI build * fix failing test * ai feedback * add buffer size checking * use c_str * dont log session key * Try this for a mac definition * be quiet apple --- cmake/FindMariaDB.cmake | 2 +- dChatServer/ChatIgnoreList.cpp | 5 +- dChatServer/ChatPacketHandler.cpp | 10 ++ dChatServer/PlayerContainer.cpp | 1 + dCommon/AMFDeserialize.cpp | 14 +- dCommon/BrickByBrickFix.cpp | 3 +- dCommon/GeneralUtils.cpp | 5 +- dCommon/GeneralUtils.h | 22 +++ dCommon/LxfmlBugged.cpp | 23 ++- dCommon/Sd0.cpp | 30 ++++ dCommon/ZCompression.cpp | 8 +- dCommon/ZCompression.h | 6 +- dCommon/dClient/AssetManager.cpp | 34 ++-- dCommon/dClient/Pack.cpp | 26 ++- dCommon/dConfig.cpp | 4 + dCommon/dConfig.h | 16 ++ dCommon/dEnums/dCommonVars.h | 4 +- .../CDClientTables/CDItemComponentTable.cpp | 4 +- .../CDClientTables/CDMissionsTable.cpp | 5 +- .../CDClientTables/CDMissionsTable.h | 1 + .../CDClientTables/CDObjectsTable.cpp | 2 +- .../CDRailActivatorComponent.cpp | 2 +- dDatabase/GameDatabase/ITables/IProperty.h | 3 +- dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 2 +- dDatabase/GameDatabase/MySQL/Tables/Mail.cpp | 3 +- .../GameDatabase/MySQL/Tables/Property.cpp | 13 +- .../GameDatabase/SQLite/SQLiteDatabase.h | 40 ++--- dDatabase/GameDatabase/SQLite/Tables/Mail.cpp | 3 +- .../GameDatabase/SQLite/Tables/Property.cpp | 13 +- .../GameDatabase/TestSQL/TestSQLDatabase.h | 2 +- dGame/Entity.h | 1 + dGame/dBehaviors/AirMovementBehavior.cpp | 25 ++- dGame/dBehaviors/AirMovementBehavior.h | 5 + dGame/dBehaviors/AreaOfEffectBehavior.cpp | 1 + dGame/dBehaviors/BasicAttackBehavior.cpp | 5 +- dGame/dBehaviors/BlockBehavior.cpp | 12 +- dGame/dBehaviors/ChainBehavior.cpp | 5 + dGame/dBehaviors/JetPackBehavior.cpp | 2 + dGame/dBehaviors/SwitchMultipleBehavior.cpp | 5 + dGame/dComponents/BaseCombatAIComponent.cpp | 1 + dGame/dComponents/BuffComponent.cpp | 17 +- dGame/dComponents/CharacterComponent.cpp | 10 +- dGame/dComponents/CharacterComponent.h | 3 +- dGame/dComponents/DestroyableComponent.cpp | 4 +- dGame/dComponents/InventoryComponent.cpp | 8 +- dGame/dComponents/LUPExhibitComponent.h | 2 +- dGame/dComponents/MissionComponent.cpp | 73 ++++---- .../MultiZoneEntranceComponent.cpp | 7 +- dGame/dComponents/PetComponent.cpp | 8 +- dGame/dComponents/PhantomPhysicsComponent.cpp | 33 +--- .../dComponents/PropertyEntranceComponent.cpp | 4 +- .../PropertyManagementComponent.cpp | 36 ++-- dGame/dComponents/QuickBuildComponent.cpp | 2 +- dGame/dComponents/RacingControlComponent.cpp | 2 +- dGame/dComponents/TriggerComponent.cpp | 26 +-- dGame/dGameMessages/GameMessageHandler.cpp | 22 ++- dGame/dGameMessages/GameMessages.cpp | 42 ++++- dGame/dInventory/Inventory.cpp | 3 +- dGame/dInventory/Item.cpp | 8 +- dGame/dInventory/ItemSet.cpp | 4 +- dGame/dMission/Mission.cpp | 16 +- dGame/dPropertyBehaviors/PropertyBehavior.cpp | 17 ++ dGame/dUtilities/BrickDatabase.cpp | 8 +- dGame/dUtilities/CheatDetection.cpp | 21 ++- dGame/dUtilities/Loot.cpp | 2 +- dGame/dUtilities/Mail.cpp | 165 ++++++++++-------- dGame/dUtilities/Preconditions.cpp | 10 +- .../SlashCommands/DEVGMCommands.cpp | 8 +- .../SlashCommands/GMZeroCommands.cpp | 7 +- dMasterServer/MasterServer.cpp | 56 +++--- dNavigation/dNavMesh.cpp | 57 +++--- dNavigation/dTerrain/RawChunk.cpp | 1 + dNet/AuthPackets.cpp | 3 +- dNet/ClientPackets.cpp | 1 + dNet/MasterPackets.cpp | 3 +- dNet/WorldPackets.cpp | 10 +- dNet/dServer.cpp | 6 +- dPhysics/dpEntity.cpp | 2 + dPhysics/dpWorld.cpp | 2 +- dScripts/02_server/Map/AM/AmSkullkinDrill.cpp | 5 +- dScripts/02_server/Map/General/Binoculars.cpp | 11 +- .../Map/General/StoryBoxInteractServer.cpp | 12 +- .../Map/NS/NsConcertChoiceBuildManager.cpp | 3 +- .../02_server/Map/NT/NtParadoxPanelServer.cpp | 3 +- .../Map/Property/AG_Small/ZoneAgProperty.cpp | 3 +- .../02_server/Map/VE/VeMissionConsole.cpp | 2 +- .../02_server/Map/njhub/CavePrisonCage.cpp | 11 +- .../boss_instance/NjMonastryBossInstance.cpp | 2 +- dScripts/BaseConsoleTeleportServer.cpp | 2 +- dScripts/BasePropertyServer.cpp | 7 +- dScripts/BaseRandomServer.cpp | 20 ++- dScripts/BaseRandomServer.h | 2 +- dScripts/BaseSurvivalServer.cpp | 3 +- dScripts/NtFactionSpyServer.cpp | 2 +- dScripts/SpawnPetBaseServer.cpp | 2 +- dScripts/ai/AG/AgFans.cpp | 13 +- dScripts/ai/FV/FvBrickPuzzleServer.cpp | 2 + dScripts/ai/FV/FvFacilityBrick.cpp | 35 ++-- .../ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp | 2 +- dScripts/ai/NS/NsConcertQuickBuild.cpp | 4 +- dScripts/ai/NS/NsGetFactionMissionServer.cpp | 7 +- dScripts/ai/PROPERTY/AG/AgPropGuard.cpp | 3 +- dServer/CMakeLists.txt | 2 + dWorldServer/WorldServer.cpp | 40 +++-- dZoneManager/Level.cpp | 24 +-- dZoneManager/Spawner.cpp | 15 +- dZoneManager/Spawner.h | 2 +- resources/chatconfig.ini | 2 + tests/dCommonTests/AMFDeserializeTests.cpp | 2 +- 109 files changed, 806 insertions(+), 514 deletions(-) diff --git a/cmake/FindMariaDB.cmake b/cmake/FindMariaDB.cmake index c76d2d3a..edae8b31 100644 --- a/cmake/FindMariaDB.cmake +++ b/cmake/FindMariaDB.cmake @@ -80,7 +80,7 @@ else() # Build from source -DWITH_EXTERNAL_ZLIB=ON -DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR} -DCMAKE_C_FLAGS=-w # disable zlib warnings - -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -include\ cstdint) + -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -Wno-inconsistent-missing-override\ -include\ cstdint) else() set(MARIADB_EXTRA_CMAKE_ARGS -DCMAKE_C_FLAGS=-w # disable zlib warnings diff --git a/dChatServer/ChatIgnoreList.cpp b/dChatServer/ChatIgnoreList.cpp index 6433da70..661e74f9 100644 --- a/dChatServer/ChatIgnoreList.cpp +++ b/dChatServer/ChatIgnoreList.cpp @@ -3,6 +3,7 @@ #include "MessageType/Chat.h" #include "BitStreamUtils.h" #include "Game.h" +#include "dConfig.h" #include "Logger.h" #include "eObjectBits.h" @@ -72,8 +73,8 @@ void ChatIgnoreList::AddIgnore(Packet* packet) { return; } - constexpr int32_t MAX_IGNORES = 32; - if (receiver.ignoredPlayers.size() > MAX_IGNORES) { + const int32_t MAX_IGNORES = Game::config->GetValue("max_ignores", 32); + if (receiver.ignoredPlayers.size() >= MAX_IGNORES) { LOG_DEBUG("Player %llu has too many ignores", playerId); return; } diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp index 844631b4..10904f4b 100644 --- a/dChatServer/ChatPacketHandler.cpp +++ b/dChatServer/ChatPacketHandler.cpp @@ -435,6 +435,11 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) { inStream.IgnoreBytes(4); inStream.Read(channel); inStream.Read(size); + if (size > MAX_MESSAGE_LENGTH) { + LOG("Received a probably spoofed chat message, ignoring msg"); + return; + } + inStream.IgnoreBytes(77); LUWString message(size); @@ -479,6 +484,11 @@ void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) { if (channel != eChatChannel::PRIVATE_CHAT) LOG("WARNING: Received Private chat with the wrong channel!"); inStream.Read(size); + if (size > MAX_MESSAGE_LENGTH) { + LOG("Received a probably spoofed chat message, ignoring msg"); + return; + } + inStream.IgnoreBytes(77); inStream.Read(LUReceiverName); diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index 6c75fd1d..148cab2a 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -176,6 +176,7 @@ LWOOBJID PlayerContainer::GetId(const std::u16string& playerName) { return toReturn; } +// TODO Make this a pointer again or do something to make it so you cant edit the LWOOBJID_EMPTY entry? it should be ignored in all cases anyways though... PlayerData& PlayerContainer::GetPlayerDataMutable(const LWOOBJID& playerID) { return m_Players.contains(playerID) ? m_Players[playerID] : m_Players[LWOOBJID_EMPTY]; } diff --git a/dCommon/AMFDeserialize.cpp b/dCommon/AMFDeserialize.cpp index 884a0784..9d46b175 100644 --- a/dCommon/AMFDeserialize.cpp +++ b/dCommon/AMFDeserialize.cpp @@ -3,6 +3,7 @@ #include <stdexcept> #include "Amf3.h" +#include "StringifiedEnum.h" /** * AMF3 Reference document https://rtmp.veriskope.com/pdf/amf3-file-format-spec.pdf @@ -53,7 +54,7 @@ std::unique_ptr<AMFBaseValue> AMFDeserialize::Read(RakNet::BitStream& inStream) case eAmf::VectorObject: [[fallthrough]]; case eAmf::Dictionary: - throw marker; + throw std::invalid_argument(StringifiedEnum::ToString(marker).data()); default: throw std::invalid_argument("Invalid AMF3 marker" + std::to_string(static_cast<int32_t>(marker))); } @@ -88,6 +89,11 @@ const std::string AMFDeserialize::ReadString(RakNet::BitStream& inStream) { // Right shift by 1 bit to get index if reference or size of next string if value length = length >> 1; if (isReference) { + constexpr int32_t maxStringSize = 1024 * 1024; + if (length > maxStringSize) { + LOG("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize."); + throw std::invalid_argument("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize."); + } std::string value(length, 0); inStream.Read(&value[0], length); // Empty strings are never sent by reference @@ -117,6 +123,12 @@ std::unique_ptr<AMFArrayValue> AMFDeserialize::ReadAmfArray(RakNet::BitStream& i if (key.size() == 0) break; arrayValue->Insert(key, Read(inStream)); } + + constexpr int32_t maxArraySize = 10'000; + if (sizeOfDenseArray > maxArraySize) { + LOG("Someone sent 10,000 dense array entries, probably a bad packet."); + throw std::invalid_argument("Someone sent 10,000 dense array entries, probably a bad packet."); + } // Finally read dense portion for (uint32_t i = 0; i < sizeOfDenseArray; i++) { arrayValue->Insert(i, Read(inStream)); diff --git a/dCommon/BrickByBrickFix.cpp b/dCommon/BrickByBrickFix.cpp index b5a2df9b..d299341f 100644 --- a/dCommon/BrickByBrickFix.cpp +++ b/dCommon/BrickByBrickFix.cpp @@ -52,8 +52,7 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() { if (actualUncompressedSize != -1) { uint32_t previousSize = completeUncompressedModel.size(); - completeUncompressedModel.append(reinterpret_cast<char*>(uncompressedChunk.get())); - completeUncompressedModel.resize(previousSize + actualUncompressedSize); + completeUncompressedModel.append(reinterpret_cast<char*>(uncompressedChunk.get()), actualUncompressedSize); } else { LOG("Failed to inflate chunk %i for model %llu. Error: %i", chunkCount, model.id, err); break; diff --git a/dCommon/GeneralUtils.cpp b/dCommon/GeneralUtils.cpp index 7cc9278b..96bf9e01 100644 --- a/dCommon/GeneralUtils.cpp +++ b/dCommon/GeneralUtils.cpp @@ -308,8 +308,9 @@ std::vector<std::string> GeneralUtils::GetSqlFileNamesFromFolder(const std::stri for (const auto& t : std::filesystem::directory_iterator(folder)) { if (t.is_directory() || t.is_symlink()) continue; auto filename = t.path().filename().string(); - const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0)); - filenames.emplace(index, std::move(filename)); + // Ensure the file has a name in the format of xxxxxxxx_anything_goes_here.sql + const auto migrationNumber = TryParse<uint32_t>(GeneralUtils::SplitString(filename, '_').at(0)); + if (migrationNumber.has_value()) filenames.emplace(migrationNumber.value(), std::move(filename)); } // Now sort the map by the oldest migration. diff --git a/dCommon/GeneralUtils.h b/dCommon/GeneralUtils.h index 828fcee3..30be70be 100644 --- a/dCommon/GeneralUtils.h +++ b/dCommon/GeneralUtils.h @@ -205,6 +205,12 @@ namespace GeneralUtils { return isParsed ? static_cast<T>(result) : std::optional<T>{}; } + // A version of TryParse that will return `errorVal` if `str` failed to parse. + template <Numeric T> + [[nodiscard]] T TryParse(std::string_view str, const T errorVal) { + return TryParse<T>(str).value_or(errorVal); + } + template<typename T> requires(!Numeric<T>) [[nodiscard]] std::optional<T> TryParse(std::string_view str); @@ -237,6 +243,12 @@ namespace GeneralUtils { return std::nullopt; } + // A version of TryParse that will return `errorVal` if `str` failed to parse. + template <std::floating_point T> + [[nodiscard]] T TryParse(std::string_view str, const T errorVal) { + return TryParse<T>(str).value_or(errorVal); + } + #endif /** @@ -258,6 +270,11 @@ namespace GeneralUtils { return z ? std::make_optional<T>(x.value(), y.value(), z.value()) : std::nullopt; } + // Alternative overload of TryParse with a default value + [[nodiscard]] inline NiPoint3 TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ, const NiPoint3 errorVal) { + return TryParse<NiPoint3>(strX, strY, strZ).value_or(errorVal); + } + /** * The TryParse overload for handling NiPoint3 by passing a span of three strings * @param str The string vector representing the X, Y, and Z coordinates @@ -268,6 +285,11 @@ namespace GeneralUtils { return (str.size() == 3) ? TryParse<T>(str[0], str[1], str[2]) : std::nullopt; } + // Alternative overload of TryParse with a default value + [[nodiscard]] inline NiPoint3 TryParse(const std::span<const std::string> str, const NiPoint3 errorVal) { + return TryParse<NiPoint3>(str).value_or(errorVal); + } + template <typename T> std::u16string to_u16string(const T value) { return GeneralUtils::ASCIIToUTF16(std::to_string(value)); diff --git a/dCommon/LxfmlBugged.cpp b/dCommon/LxfmlBugged.cpp index f5eeebc8..fcd56ee0 100644 --- a/dCommon/LxfmlBugged.cpp +++ b/dCommon/LxfmlBugged.cpp @@ -53,16 +53,21 @@ Lxfml::Result Lxfml::NormalizePositionOnlyFirstPart(const std::string_view data) continue; } - auto x = GeneralUtils::TryParse<float>(split[9]).value(); - auto y = GeneralUtils::TryParse<float>(split[10]).value(); - auto z = GeneralUtils::TryParse<float>(split[11]).value(); - if (x < lowest.x) lowest.x = x; - if (y < lowest.y) lowest.y = y; - if (z < lowest.z) lowest.z = z; + try { + auto x = GeneralUtils::TryParse<float>(split[9]).value(); + auto y = GeneralUtils::TryParse<float>(split[10]).value(); + auto z = GeneralUtils::TryParse<float>(split[11]).value(); + if (x < lowest.x) lowest.x = x; + if (y < lowest.y) lowest.y = y; + if (z < lowest.z) lowest.z = z; - if (highest.x < x) highest.x = x; - if (highest.y < y) highest.y = y; - if (highest.z < z) highest.z = z; + if (highest.x < x) highest.x = x; + if (highest.y < y) highest.y = y; + if (highest.z < z) highest.z = z; + } catch (std::exception& e) { + LOG("Failed to parse a split value of either (%s), (%s), or (%s).", split[9].c_str(), split[10].c_str(), split[11].c_str()); + return toReturn; // Early return since we failed to parse this lxfml. + } } auto delta = (highest - lowest) / 2.0f; diff --git a/dCommon/Sd0.cpp b/dCommon/Sd0.cpp index 5b97cb75..e06f242e 100644 --- a/dCommon/Sd0.cpp +++ b/dCommon/Sd0.cpp @@ -45,6 +45,12 @@ Sd0::Sd0(std::istream& buffer) { uint32_t bufferSize = buffer.tellg(); buffer.seekg(0, std::ios::beg); WriteSize(firstChunk, bufferSize); + // its expected that if we got here, we got an old sd0 buffer where we ignored the sd0 part + // that means this can be at most the compressed chunk limit. + if (bufferSize > MAX_UNCOMPRESSED_CHUNK_SIZE) { + LOG("Possible bad chunk size of %i specified, rejecting.", bufferSize); + return; + } firstChunk.resize(firstChunk.size() + bufferSize); auto* dataStart = reinterpret_cast<char*>(firstChunk.data() + GetDataOffset(true)); if (!buffer.read(dataStart, bufferSize)) { @@ -71,6 +77,12 @@ Sd0::Sd0(std::istream& buffer) { WriteSize(chunk, chunkSize); + // Assuming a good buffer that is large enough to take up 2 zlib buffers + // any buffer should be compressed enough to take up less size than its uncompressed counterpart + if (chunkSize > MAX_UNCOMPRESSED_CHUNK_SIZE) { + LOG("Possible bad chunk size of %i specified, rejecting.", chunkSize); + break; + } chunk.resize(chunkSize + dataOffset); auto* dataStart = reinterpret_cast<char*>(chunk.data() + dataOffset); if (!buffer.read(dataStart, chunkSize)) { @@ -95,6 +107,11 @@ void Sd0::FromData(const uint8_t* data, size_t bufferSize) { startOffset, numToCopy, compressedChunk.data(), compressedChunk.size()); + if (compressedSize == -1) { + LOG("Failed to compress chunk, aborting"); + break; + } + auto& chunk = m_Chunks.emplace_back(); bool firstBuffer = m_Chunks.size() == 1; auto dataOffset = GetDataOffset(firstBuffer); @@ -119,6 +136,12 @@ std::string Sd0::GetAsStringUncompressed() const { auto dataOffset = GetDataOffset(first); first = false; const auto chunkSize = chunk.size(); + if (chunkSize <= static_cast<size_t>(dataOffset)) { + LOG("Bad chunkSize for data, aborting"); + toReturn = ""; + totalSize = 0; + break; + } auto oldSize = toReturn.size(); toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE); @@ -128,6 +151,13 @@ std::string Sd0::GetAsStringUncompressed() const { reinterpret_cast<uint8_t*>(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE, error); + if (uncompressedSize == -1) { + LOG("Failed to decompress chunk, aborting"); + toReturn = ""; + totalSize = 0; + break; + } + totalSize += uncompressedSize; } diff --git a/dCommon/ZCompression.cpp b/dCommon/ZCompression.cpp index e5b3c8fb..11ec4e83 100644 --- a/dCommon/ZCompression.cpp +++ b/dCommon/ZCompression.cpp @@ -3,12 +3,12 @@ #include "zlib.h" namespace ZCompression { - int32_t GetMaxCompressedLength(int32_t nLenSrc) { - int32_t n16kBlocks = (nLenSrc + 16383) / 16384; // round up any fraction of a block + uint32_t GetMaxCompressedLength(uint32_t nLenSrc) { + uint32_t n16kBlocks = (nLenSrc + 16383) / 16384; // round up any fraction of a block return (nLenSrc + 6 + (n16kBlocks * 5)); } - int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst) { + int32_t Compress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst) { z_stream zInfo = { 0 }; zInfo.total_in = zInfo.avail_in = nLenSrc; zInfo.total_out = zInfo.avail_out = nLenDst; @@ -27,7 +27,7 @@ namespace ZCompression { return(nRet); } - int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr) { + int32_t Decompress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst, int32_t& nErr) { // Get the size of the decompressed data z_stream zInfo = { 0 }; zInfo.total_in = zInfo.avail_in = nLenSrc; diff --git a/dCommon/ZCompression.h b/dCommon/ZCompression.h index 22a5ff86..d58a8778 100644 --- a/dCommon/ZCompression.h +++ b/dCommon/ZCompression.h @@ -3,10 +3,10 @@ #include <cstdint> namespace ZCompression { - int32_t GetMaxCompressedLength(int32_t nLenSrc); + uint32_t GetMaxCompressedLength(uint32_t nLenSrc); - int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst); + int32_t Compress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst); - int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr); + int32_t Decompress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst, int32_t& nErr); } diff --git a/dCommon/dClient/AssetManager.cpp b/dCommon/dClient/AssetManager.cpp index bc4fbf35..92a458d3 100644 --- a/dCommon/dClient/AssetManager.cpp +++ b/dCommon/dClient/AssetManager.cpp @@ -7,7 +7,7 @@ #include "zlib.h" constexpr uint32_t CRC32_INIT = 0xFFFFFFFF; -constexpr auto NULL_TERMINATOR = std::string_view{"\0\0\0", 4}; +constexpr auto NULL_TERMINATOR = std::string_view{ "\0\0\0", 4 }; AssetManager::AssetManager(const std::filesystem::path& path) { if (!std::filesystem::is_directory(path)) { @@ -25,7 +25,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) { if (!std::filesystem::exists(m_Path / ".." / "versions")) { throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded."); } - + m_AssetBundleType = eAssetBundleType::Packed; m_RootPath = (m_Path / ".."); @@ -34,7 +34,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) { if (!std::filesystem::exists(m_Path / ".." / ".." / "versions")) { throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded."); } - + m_AssetBundleType = eAssetBundleType::Packed; m_RootPath = (m_Path / ".." / ".."); @@ -54,15 +54,15 @@ AssetManager::AssetManager(const std::filesystem::path& path) { } switch (m_AssetBundleType) { - case eAssetBundleType::Packed: { - this->LoadPackIndex(); - break; - } - case eAssetBundleType::None: - [[fallthrough]]; - case eAssetBundleType::Unpacked: { - break; - } + case eAssetBundleType::Packed: { + this->LoadPackIndex(); + break; + } + case eAssetBundleType::None: + [[fallthrough]]; + case eAssetBundleType::Unpacked: { + break; + } } } @@ -79,7 +79,7 @@ bool AssetManager::HasFile(std::string fixedName) const { std::replace(fixedName.begin(), fixedName.end(), '\\', '/'); if (std::filesystem::exists(m_ResPath / fixedName)) return true; - if (this->m_AssetBundleType == eAssetBundleType::Unpacked) return false; + if (this->m_AssetBundleType == eAssetBundleType::Unpacked || !m_PackIndex) return false; std::replace(fixedName.begin(), fixedName.end(), '/', '\\'); if (fixedName.rfind("client\\res\\", 0) != 0) fixedName = "client\\res\\" + fixedName; @@ -145,8 +145,12 @@ bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) co } const auto& pack = this->m_PackIndex->GetPacks().at(packIndex); - const bool success = pack.ReadFileFromPack(crc, data, len); - + bool success = false; + try { + success = pack.ReadFileFromPack(crc, data, len); + } catch (std::exception& e) { + LOG("Failed to read file %s from pack file", fixedName.c_str()); + } return success; } diff --git a/dCommon/dClient/Pack.cpp b/dCommon/dClient/Pack.cpp index 800cfa19..0f558a54 100644 --- a/dCommon/dClient/Pack.cpp +++ b/dCommon/dClient/Pack.cpp @@ -46,6 +46,7 @@ bool Pack::HasFile(const uint32_t crc) const { } bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) const { + const auto pathStr = m_FilePath.string(); // Time for some wacky C file reading for speed reasons PackRecord pkRecord{}; @@ -65,16 +66,21 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons bool isCompressed = (pkRecord.m_IsCompressed & 0xff) > 0; auto inPackSize = isCompressed ? pkRecord.m_CompressedSize : pkRecord.m_UncompressedSize; - FILE* file; + FILE* file = nullptr; #ifdef _WIN32 - fopen_s(&file, m_FilePath.string().c_str(), "rb"); + fopen_s(&file, pathStr.c_str(), "rb"); #elif __APPLE__ // macOS has 64bit file IO by default - file = fopen(m_FilePath.string().c_str(), "rb"); + file = fopen(pathStr.c_str(), "rb"); #else - file = fopen64(m_FilePath.string().c_str(), "rb"); + file = fopen64(pathStr.c_str(), "rb"); #endif + if (!file) { + LOG("No file found for path %s", pathStr.c_str()); + throw std::runtime_error("Could not find file " + pathStr); + } + fseek(file, pos, SEEK_SET); if (!isCompressed) { @@ -102,14 +108,18 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons int32_t readInData = fread(&size, sizeof(uint32_t), 1, file); pos += 4; // Move pointer position 4 to the right - char* chunk = static_cast<char*>(malloc(size)); - int32_t readInData2 = fread(chunk, sizeof(int8_t), size, file); + std::unique_ptr<char[]> chunk(new char[size]); + int32_t readInData2 = fread(chunk.get(), sizeof(int8_t), size, file); pos += size; // Move pointer position the amount of bytes read to the right int32_t err; - currentReadPos += ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); + const auto countToRead = ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk.get()), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); + if (countToRead == -1) { + LOG("Error decompressing zlib data from file %s", pathStr.c_str()); + throw std::runtime_error("Error decompressing zlib data from file " + pathStr); + } + currentReadPos += countToRead; - free(chunk); } *data = decompressedData; diff --git a/dCommon/dConfig.cpp b/dCommon/dConfig.cpp index 7ebfad25..21a0746d 100644 --- a/dCommon/dConfig.cpp +++ b/dCommon/dConfig.cpp @@ -84,3 +84,7 @@ void dConfig::ProcessLine(const std::string& line) { this->m_ConfigValues.insert(std::make_pair(key, value)); } + +std::string dConfig::GetValue(const std::string& key, const char* emptyValue) { + return GetValue(key, std::string(emptyValue)); +}; diff --git a/dCommon/dConfig.h b/dCommon/dConfig.h index 36c94148..8e645caa 100644 --- a/dCommon/dConfig.h +++ b/dCommon/dConfig.h @@ -5,6 +5,8 @@ #include <map> #include <string> +#include "GeneralUtils.h" + class dConfig { public: dConfig(const std::string& filepath); @@ -22,6 +24,14 @@ public: */ const std::string& GetValue(std::string key); + // Gets a value from the config and returns the parsed value, or the default value should parsing have failed. + template<typename T> + T GetValue(const std::string& key, const T emptyValue = T()) { + return GeneralUtils::TryParse<T>(GetValue(key)).value_or(emptyValue); + } + + std::string GetValue(const std::string& key, const char* emptyValue); + /** * Loads the config from a file */ @@ -43,3 +53,9 @@ private: std::vector<std::function<void()>> m_ConfigHandlers; std::string m_ConfigFilePath; }; + +template<> +inline std::string dConfig::GetValue(const std::string& key, const std::string emptyValue) { + const auto& value = GetValue(key); + return value.empty() ? emptyValue : value; +}; diff --git a/dCommon/dEnums/dCommonVars.h b/dCommon/dEnums/dCommonVars.h index c00c4be5..fc1ccded 100644 --- a/dCommon/dEnums/dCommonVars.h +++ b/dCommon/dEnums/dCommonVars.h @@ -16,8 +16,8 @@ // These are the same define, but they mean two different things in different contexts // so a different define to distinguish what calculation is happening will help clarity. -#define FRAMES_TO_MS(x) 1000 / x -#define MS_TO_FRAMES(x) 1000 / x +#define FRAMES_TO_MS(x) (1000 / (x)) +#define MS_TO_FRAMES(x) (1000 / (x)) //=========== FRAME TIMINGS =========== constexpr uint32_t highFramerate = 60; diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp index 0d8b1ad9..8b69e262 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp @@ -154,8 +154,8 @@ std::map<LOT, uint32_t> CDItemComponentTable::ParseCraftingCurrencies(const CDIt // Checking for 2 here, not sure what to do when there's more stuff than expected if (amountSplit.size() == 2) { currencies.insert({ - std::stoull(amountSplit[0]), - std::stoi(amountSplit[1]) + GeneralUtils::TryParse<LOT>(amountSplit[0], LOT_NULL), + GeneralUtils::TryParse<uint32_t>(amountSplit[1], 0) }); } } diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp index c98254ea..6466d64c 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp @@ -93,13 +93,14 @@ std::vector<CDMissions> CDMissionsTable::Query(std::function<bool(CDMissions)> p } const CDMissions* CDMissionsTable::GetPtrByMissionID(uint32_t missionID) const { + const CDMissions* toReturn = &Default; for (const auto& entry : GetEntries()) { if (entry.id == missionID) { - return const_cast<CDMissions*>(&entry); + toReturn = &entry; } } - return &Default; + return toReturn; } const CDMissions& CDMissionsTable::GetByMissionID(uint32_t missionID, bool& found) const { diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h index c5ae0e88..fea37bd6 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h @@ -66,6 +66,7 @@ public: // Queries the table with a custom "where" clause std::vector<CDMissions> Query(std::function<bool(CDMissions)> predicate); + // Cannot be null. const CDMissions* GetPtrByMissionID(uint32_t missionID) const; const CDMissions& GetByMissionID(uint32_t missionID, bool& found) const; diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp index 738a13ac..ede9dad7 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp @@ -69,7 +69,7 @@ const CDObjects& CDObjectsTable::GetByID(const uint32_t lot) { entry.name = tableData.getStringField("name", ""); UNUSED(entry.placeable = tableData.getIntField("placeable", -1)); entry.type = tableData.getStringField("type", ""); - UNUSED(ntry.description = tableData.getStringField(4, "")); + UNUSED(entry.description = tableData.getStringField(4, "")); UNUSED(entry.localize = tableData.getIntField("localize", -1)); UNUSED(entry.npcTemplateID = tableData.getIntField("npcTemplateID", -1)); UNUSED(entry.displayName = tableData.getStringField("displayName", "")); diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp index 72a26beb..277a5cee 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp @@ -56,7 +56,7 @@ CDRailActivatorComponent CDRailActivatorComponentTable::GetEntryByID(int32_t id) std::pair<uint32_t, std::u16string> CDRailActivatorComponentTable::EffectPairFromString(std::string& str) { const auto split = GeneralUtils::SplitString(str, ':'); if (split.size() == 2) { - return { std::stoi(split.at(0)), GeneralUtils::ASCIIToUTF16(split.at(1)) }; + return { GeneralUtils::TryParse(split.at(0), 0), GeneralUtils::ASCIIToUTF16(split.at(1)) }; } return {}; diff --git a/dDatabase/GameDatabase/ITables/IProperty.h b/dDatabase/GameDatabase/ITables/IProperty.h index c4957e5b..24171576 100644 --- a/dDatabase/GameDatabase/ITables/IProperty.h +++ b/dDatabase/GameDatabase/ITables/IProperty.h @@ -34,6 +34,7 @@ public: }; struct PropertyEntranceResult { + // This is the number of entries that are in the query IF it were ran without a limit. int32_t totalEntriesMatchingQuery{}; // The entries that match the query. This should only contain up to 12 entries. std::vector<IProperty::Info> entries; @@ -48,7 +49,7 @@ public: // Get the properties for the given property lookup params. // This is expected to return a result set of up to 12 properties // so as not to transfer too much data at once. - virtual std::optional<IProperty::PropertyEntranceResult> GetProperties(const PropertyLookup& params) = 0; + virtual IProperty::PropertyEntranceResult GetProperties(const PropertyLookup& params) = 0; // Update the property moderation info for the given property id. virtual void UpdatePropertyModerationInfo(const IProperty::Info& info) = 0; diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index b481877c..accfb27e 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -127,7 +127,7 @@ public: std::string GetBehavior(const LWOOBJID behaviorId) override; void RemoveBehavior(const LWOOBJID characterId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; - std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override; + IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override; std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override; std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override; std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp b/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp index 27d76899..c6900492 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp @@ -48,7 +48,7 @@ std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId } std::optional<MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) { - auto res = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId); + auto res = ExecuteSelect("SELECT attachment_lot, attachment_count, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId); if (!res->next()) { return std::nullopt; @@ -57,6 +57,7 @@ std::optional<MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) { MailInfo toReturn; toReturn.itemLOT = res->getInt("attachment_lot"); toReturn.itemCount = res->getInt("attachment_count"); + toReturn.receiverId = res->getUInt64("receiver_id"); return toReturn; } diff --git a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp index d554a21d..ee958c80 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp @@ -18,8 +18,8 @@ IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) { return info; } -std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) { - std::optional<IProperty::PropertyEntranceResult> result; +IProperty::PropertyEntranceResult MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) { + IProperty::PropertyEntranceResult result; std::string query; PreparedStmtResultSet properties; @@ -73,8 +73,7 @@ std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(co params.playerId ); if (count->next()) { - if (!result) result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count->getUInt("count"); + result.totalEntriesMatchingQuery = count->getUInt("count"); } } else { if (params.sortChoice == SORT_TYPE_REPUTATION) { @@ -127,14 +126,12 @@ std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(co params.playerSort ); if (count->next()) { - if (!result) result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count->getUInt("count"); + result.totalEntriesMatchingQuery = count->getUInt("count"); } } while (properties->next()) { - if (!result) result = IProperty::PropertyEntranceResult(); - result->entries.push_back(ReadPropertyInfo(properties)); + result.entries.push_back(ReadPropertyInfo(properties)); } return result; diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 3b6dc643..afcd0d26 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -111,7 +111,7 @@ public: std::string GetBehavior(const LWOOBJID behaviorId) override; void RemoveBehavior(const LWOOBJID characterId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; - std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override; + IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override; std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override; std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override; std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override; @@ -170,91 +170,91 @@ private: template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::string_view param) { - LOG("%s", param.data()); + LOG_DEBUG("%s", param.data()); stmt.bind(index, param.data()); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const char* param) { - LOG("%s", param); + LOG_DEBUG("%s", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::string param) { - LOG("%s", param.c_str()); + LOG_DEBUG("%s", param.c_str()); stmt.bind(index, param.c_str()); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int8_t param) { - LOG("%u", param); + LOG_DEBUG("%u", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint8_t param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int16_t param) { - LOG("%u", param); + LOG_DEBUG("%u", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint16_t param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint32_t param) { - LOG("%u", param); + LOG_DEBUG("%u", param); stmt.bind(index, static_cast<int32_t>(param)); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int32_t param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int64_t param) { - LOG("%llu", param); + LOG_DEBUG("%llu", param); stmt.bind(index, static_cast<sqlite_int64>(param)); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint64_t param) { - LOG("%llu", param); + LOG_DEBUG("%llu", param); stmt.bind(index, static_cast<sqlite_int64>(param)); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const float param) { - LOG("%f", param); + LOG_DEBUG("%f", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const double param) { - LOG("%f", param); + LOG_DEBUG("%f", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const bool param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::istream* param) { - LOG("Blob"); + LOG_DEBUG("Blob"); // This is the one time you will ever see me use const_cast. std::stringstream stream; stream << param->rdbuf(); @@ -264,10 +264,10 @@ inline void SetParam(PreppedStmtRef stmt, const int index, const std::istream* p template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<uint32_t> param) { if (param) { - LOG("%d", param.value()); + LOG_DEBUG("%d", param.value()); stmt.bind(index, static_cast<int>(param.value())); } else { - LOG("Null"); + LOG_DEBUG("Null"); stmt.bindNull(index); } } @@ -275,10 +275,10 @@ inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<u template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<LWOOBJID> param) { if (param) { - LOG("%d", param.value()); + LOG_DEBUG("%d", param.value()); stmt.bind(index, static_cast<sqlite_int64>(param.value())); } else { - LOG("Null"); + LOG_DEBUG("Null"); stmt.bindNull(index); } } diff --git a/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp b/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp index 7b0a4860..a101beaa 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp @@ -47,7 +47,7 @@ std::vector<MailInfo> SQLiteDatabase::GetMailForPlayer(const LWOOBJID characterI } std::optional<MailInfo> SQLiteDatabase::GetMail(const uint64_t mailId) { - auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId); + auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId); if (res.eof()) { return std::nullopt; @@ -56,6 +56,7 @@ std::optional<MailInfo> SQLiteDatabase::GetMail(const uint64_t mailId) { MailInfo toReturn; toReturn.itemLOT = res.getIntField("attachment_lot"); toReturn.itemCount = res.getIntField("attachment_count"); + toReturn.receiverId = res.getInt64Field("receiver_id"); return toReturn; } diff --git a/dDatabase/GameDatabase/SQLite/Tables/Property.cpp b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp index 67fc57b3..caff8441 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Property.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp @@ -18,8 +18,8 @@ IProperty::Info ReadPropertyInfo(CppSQLite3Query& propertyEntry) { return toReturn; } -std::optional<IProperty::PropertyEntranceResult> SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) { - std::optional<IProperty::PropertyEntranceResult> result; +IProperty::PropertyEntranceResult SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) { + IProperty::PropertyEntranceResult result; std::string query; std::pair<CppSQLite3Statement, CppSQLite3Query> propertiesRes; @@ -73,8 +73,7 @@ std::optional<IProperty::PropertyEntranceResult> SQLiteDatabase::GetProperties(c params.playerId ); if (!count.eof()) { - result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count.getIntField("count"); + result.totalEntriesMatchingQuery = count.getIntField("count"); } } else { if (params.sortChoice == SORT_TYPE_REPUTATION) { @@ -127,15 +126,13 @@ std::optional<IProperty::PropertyEntranceResult> SQLiteDatabase::GetProperties(c params.playerSort ); if (!count.eof()) { - result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count.getIntField("count"); + result.totalEntriesMatchingQuery = count.getIntField("count"); } } auto& [_, properties] = propertiesRes; - if (!properties.eof() && !result.has_value()) result = IProperty::PropertyEntranceResult(); while (!properties.eof()) { - result->entries.push_back(ReadPropertyInfo(properties)); + result.entries.push_back(ReadPropertyInfo(properties)); properties.nextRow(); } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 2c7890dd..b8706c49 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -90,7 +90,7 @@ class TestSQLDatabase : public GameDatabase { std::string GetBehavior(const LWOOBJID behaviorId) override; void RemoveBehavior(const LWOOBJID behaviorId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; - std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override { return {}; }; + IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override { return {}; }; std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override { return {}; }; std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override { return {}; }; std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override { return {}; }; diff --git a/dGame/Entity.h b/dGame/Entity.h index 565c906c..c07bd3f4 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -112,6 +112,7 @@ public: uint16_t GetNetworkId() const; + // Cannot return nullptr. Entity* GetOwner() const; const NiPoint3& GetDefaultPosition() const; diff --git a/dGame/dBehaviors/AirMovementBehavior.cpp b/dGame/dBehaviors/AirMovementBehavior.cpp index 46d18680..ad8fe541 100644 --- a/dGame/dBehaviors/AirMovementBehavior.cpp +++ b/dGame/dBehaviors/AirMovementBehavior.cpp @@ -30,6 +30,21 @@ void AirMovementBehavior::Sync(BehaviorContext* context, RakNet::BitStream& bitS return; } + // So a player can't send an arbitrary behaviorID in a modified client and cast any behavior on any air behavior + Behavior* toSync = nullptr; + if (m_GroundAction->GetBehaviorID() == behaviorId) { + toSync = m_GroundAction; + } else if (m_HitAction->GetBehaviorID() == behaviorId) { + toSync = m_HitAction; + } else if (m_HitActionEnemy->GetBehaviorID() == behaviorId) { + toSync = m_HitActionEnemy; + } else if (m_TimeoutAction->GetBehaviorID() == behaviorId) { + toSync = m_TimeoutAction; + } else { + LOG("Invalid Air Movement Behavior sync for behaviorID %i on behavior %i", behaviorId, m_behaviorId); + return; + } + LWOOBJID target{}; if (!bitStream.Read(target)) { @@ -37,15 +52,17 @@ void AirMovementBehavior::Sync(BehaviorContext* context, RakNet::BitStream& bitS return; } - auto* behavior = CreateBehavior(behaviorId); - if (Game::entityManager->GetEntity(target) != nullptr) { branch.target = target; } - behavior->Handle(context, bitStream, branch); + toSync->Handle(context, bitStream, branch); } void AirMovementBehavior::Load() { - this->m_Timeout = (GetFloat("timeout_ms") / 1000.0f); + m_Timeout = (GetFloat("timeout_ms") / 1000.0f); + m_GroundAction = GetAction("ground_action"); + m_HitAction = GetAction("hit_action"); + m_HitActionEnemy = GetAction("hit_action_enemy"); + m_TimeoutAction = GetAction("timeout_action"); } diff --git a/dGame/dBehaviors/AirMovementBehavior.h b/dGame/dBehaviors/AirMovementBehavior.h index 0edb1a76..cfa2dbf2 100644 --- a/dGame/dBehaviors/AirMovementBehavior.h +++ b/dGame/dBehaviors/AirMovementBehavior.h @@ -15,4 +15,9 @@ public: void Load() override; private: float m_Timeout; + + Behavior* m_GroundAction{}; + Behavior* m_HitAction{}; + Behavior* m_HitActionEnemy{}; + Behavior* m_TimeoutAction{}; }; diff --git a/dGame/dBehaviors/AreaOfEffectBehavior.cpp b/dGame/dBehaviors/AreaOfEffectBehavior.cpp index ce41785f..6f1b76fd 100644 --- a/dGame/dBehaviors/AreaOfEffectBehavior.cpp +++ b/dGame/dBehaviors/AreaOfEffectBehavior.cpp @@ -42,6 +42,7 @@ void AreaOfEffectBehavior::Handle(BehaviorContext* context, RakNet::BitStream& b LWOOBJID target{}; if (!bitStream.Read(target)) { LOG("failed to read in target %i from bitStream, aborting target Handle!", i); + continue; }; targets.push_back(target); } diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index 41b02e7c..e265a034 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -68,7 +68,7 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit } if (isBlocked) { - destroyableComponent->SetAttacksToBlock(std::min(destroyableComponent->GetAttacksToBlock() - 1, 0U)); + destroyableComponent->SetAttacksToBlock(std::max<int32_t>(static_cast<int32_t>(destroyableComponent->GetAttacksToBlock() - 1), 0)); Game::entityManager->SerializeEntity(targetEntity); this->m_OnFailBlocked->Handle(context, bitStream, branch); return; @@ -103,9 +103,10 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit return; } - uint32_t totalDamageDealt = armorDamageDealt + healthDamageDealt; + uint64_t totalDamageDealt = armorDamageDealt + healthDamageDealt; // A value that's too large may be a cheating attempt, so we set it to MIN + // Can't overflow here either because should we somehow get to a 64 bit number it'll be clamped to a sane value. if (totalDamageDealt > this->m_MaxDamage) { totalDamageDealt = this->m_MinDamage; } diff --git a/dGame/dBehaviors/BlockBehavior.cpp b/dGame/dBehaviors/BlockBehavior.cpp index 0d3e164b..8a7c8a64 100644 --- a/dGame/dBehaviors/BlockBehavior.cpp +++ b/dGame/dBehaviors/BlockBehavior.cpp @@ -48,15 +48,13 @@ void BlockBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext branc return; } - auto* destroyableComponent = entity->GetComponent<DestroyableComponent>(); + auto* const destroyableComponent = entity->GetComponent<DestroyableComponent>(); - destroyableComponent->SetAttacksToBlock(this->m_numAttacksCanBlock); - - if (destroyableComponent == nullptr) { - return; + if (destroyableComponent) { + // ??? what is going on here? + destroyableComponent->SetAttacksToBlock(this->m_numAttacksCanBlock); + destroyableComponent->SetAttacksToBlock(0); } - - destroyableComponent->SetAttacksToBlock(0); } void BlockBehavior::Timer(BehaviorContext* context, BehaviorBranchContext branch, LWOOBJID second) { diff --git a/dGame/dBehaviors/ChainBehavior.cpp b/dGame/dBehaviors/ChainBehavior.cpp index feb27988..237c5280 100644 --- a/dGame/dBehaviors/ChainBehavior.cpp +++ b/dGame/dBehaviors/ChainBehavior.cpp @@ -11,6 +11,11 @@ void ChainBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStrea return; } + if (chainIndex == 0) { + LOG("Received invalid chain index of 0 for behavior %i.", m_behaviorId); + return; + } + chainIndex--; if (chainIndex < this->m_behaviors.size()) { diff --git a/dGame/dBehaviors/JetPackBehavior.cpp b/dGame/dBehaviors/JetPackBehavior.cpp index 8cfb87da..78f25fcd 100644 --- a/dGame/dBehaviors/JetPackBehavior.cpp +++ b/dGame/dBehaviors/JetPackBehavior.cpp @@ -7,6 +7,7 @@ void JetPackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bit_stream, const BehaviorBranchContext branch) { auto* entity = Game::entityManager->GetEntity(branch.target); + if (!entity) return; GameMessages::SendSetJetPackMode(entity, true, this->m_BypassChecks, this->m_EnableHover, this->m_effectId, this->m_Airspeed, this->m_MaxAirspeed, this->m_VerticalVelocity, this->m_WarningEffectID); @@ -21,6 +22,7 @@ void JetPackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bit_st void JetPackBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext branch) { auto* entity = Game::entityManager->GetEntity(branch.target); + if (!entity) return; GameMessages::SendSetJetPackMode(entity, false); diff --git a/dGame/dBehaviors/SwitchMultipleBehavior.cpp b/dGame/dBehaviors/SwitchMultipleBehavior.cpp index 00d639d5..ef7309b5 100644 --- a/dGame/dBehaviors/SwitchMultipleBehavior.cpp +++ b/dGame/dBehaviors/SwitchMultipleBehavior.cpp @@ -17,6 +17,11 @@ void SwitchMultipleBehavior::Handle(BehaviorContext* context, RakNet::BitStream& return; }; + if (m_behaviors.empty()) { + LOG("No behaviors were loaded for %i, aborting call.", m_behaviorId); + return; + } + uint32_t trigger = 0; for (unsigned int i = 0; i < this->m_behaviors.size(); i++) { diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index 2bad800b..5f6399da 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -478,6 +478,7 @@ std::vector<LWOOBJID> BaseCombatAIComponent::GetTargetWithinAggroRange() const { for (auto id : m_Parent->GetTargetsInPhantom()) { auto* other = Game::entityManager->GetEntity(id); + if (!other) continue; const auto distance = Vector3::DistanceSquared(m_Parent->GetPosition(), other->GetPosition()); diff --git a/dGame/dComponents/BuffComponent.cpp b/dGame/dComponents/BuffComponent.cpp index bf09d2ea..8d699b54 100644 --- a/dGame/dComponents/BuffComponent.cpp +++ b/dGame/dComponents/BuffComponent.cpp @@ -450,19 +450,10 @@ const std::vector<BuffParameter>& BuffComponent::GetBuffParameters(int32_t buffI param.value = result.getFloatField("NumberValue"); param.effectId = result.getIntField("EffectID"); - if (!result.fieldIsNull("StringValue")) { - std::istringstream stream(result.getStringField("StringValue")); - std::string token; - - while (std::getline(stream, token, ',')) { - try { - const auto value = std::stof(token); - - param.values.push_back(value); - } catch (std::invalid_argument& exception) { - LOG("Failed to parse value (%s): (%s)!", token.c_str(), exception.what()); - } - } + for (const auto& str : GeneralUtils::SplitString(result.getStringField("StringValue"), ',')) { + if (str.empty()) continue; + const auto value = GeneralUtils::TryParse<float>(str); + if (value) param.values.push_back(value.value()); } parameters.push_back(param); diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index dda9d559..a703d884 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -797,8 +797,14 @@ std::string CharacterComponent::StatisticsToString() const { return result.str(); } -uint64_t CharacterComponent::GetStatisticFromSplit(std::vector<std::string> split, uint32_t index) { - return split.size() > index ? std::stoull(split.at(index)) : 0; +uint64_t CharacterComponent::GetStatisticFromSplit(const std::vector<std::string>& split, const uint32_t index) { + uint64_t toReturn = 0; + if (index < split.size()) { + const auto parsed = GeneralUtils::TryParse<uint64_t>(split[index]); + if (parsed) toReturn = *parsed; + } + + return toReturn; } ZoneStatistics& CharacterComponent::GetZoneStatisticsForMap(LWOMAPID mapID) { diff --git a/dGame/dComponents/CharacterComponent.h b/dGame/dComponents/CharacterComponent.h index 6f5eeaab..a37dda43 100644 --- a/dGame/dComponents/CharacterComponent.h +++ b/dGame/dComponents/CharacterComponent.h @@ -450,7 +450,7 @@ private: * @param index the statistics ID in the string * @return the integer value of this statistic, parsed from the string */ - static uint64_t GetStatisticFromSplit(std::vector<std::string> split, uint32_t index); + static uint64_t GetStatisticFromSplit(const std::vector<std::string>& split, const uint32_t index); /** * Gets all the statistics for a certain map, if it doesn't exist, it creates empty stats @@ -526,6 +526,7 @@ private: /** * Total amount of meters traveled by this character + * Should be a double and then truncated so decimals can be tracked */ uint64_t m_MetersTraveled; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index b4250966..ebccc662 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -793,7 +793,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType std::vector<Entity*> scriptedActs = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::SCRIPTED_ACTIVITY); for (Entity* scriptEntity : scriptedActs) { - if (scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds + if (!zoneControl || scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds scriptEntity->GetScript()->OnPlayerDied(scriptEntity, m_Parent); } } @@ -964,6 +964,8 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) { if (m_Parent->IsPlayer()) { //remove hardcore_lose_uscore_on_death_percent from the player's uscore: auto* character = m_Parent->GetComponent<CharacterComponent>(); + if (!character) return; + auto uscore = character->GetUScore(); auto uscoreToLose = static_cast<uint64_t>(uscore * (Game::entityManager->GetHardcoreLoseUscoreOnDeathPercent() / 100.0f)); diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 2ff827d8..a7e8d5ed 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -966,8 +966,9 @@ void InventoryComponent::EquipScripts(Item* equippedItem) { auto* itemScript = CppScripts::GetScript(m_Parent, scriptCompData.script_name); if (!itemScript) { LOG("null script?"); + } else { + itemScript->OnFactionTriggerItemEquipped(m_Parent, equippedItem->GetId()); } - itemScript->OnFactionTriggerItemEquipped(m_Parent, equippedItem->GetId()); } } @@ -981,8 +982,9 @@ void InventoryComponent::UnequipScripts(Item* unequippedItem) { auto* itemScript = CppScripts::GetScript(m_Parent, scriptCompData.script_name); if (!itemScript) { LOG("null script?"); + } else { + itemScript->OnFactionTriggerItemUnequipped(m_Parent, unequippedItem->GetId()); } - itemScript->OnFactionTriggerItemUnequipped(m_Parent, unequippedItem->GetId()); } } @@ -1633,7 +1635,7 @@ void InventoryComponent::LoadPetXml(const tinyxml2::XMLDocument& document) { DatabasePet databasePet; databasePet.lot = lot; databasePet.moderationState = moderationStatus; - databasePet.name = std::string(name); + databasePet.name = name ? name : ""; SetDatabasePet(id, databasePet); diff --git a/dGame/dComponents/LUPExhibitComponent.h b/dGame/dComponents/LUPExhibitComponent.h index 4e93dbf9..f3450304 100644 --- a/dGame/dComponents/LUPExhibitComponent.h +++ b/dGame/dComponents/LUPExhibitComponent.h @@ -22,7 +22,7 @@ public: void NextLUPExhibit(); private: float m_UpdateTimer = 0.0f; - std::array<LOT, 4> m_LUPExhibits = { 11121, 11295, 11423, 11979 }; + const std::array<LOT, 4> m_LUPExhibits = { 11121, 11295, 11423, 11979 }; uint8_t m_LUPExhibitIndex = 0; bool m_DirtyLUPExhibit = true; }; diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index e7c03379..08c4ce8d 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -449,45 +449,48 @@ void MissionComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) { if (mis == nullptr) return; - auto* cur = mis->FirstChildElement("cur"); auto* done = mis->FirstChildElement("done"); - - auto* doneM = done->FirstChildElement(); - - while (doneM) { - int missionId; - - doneM->QueryAttribute("id", &missionId); - - auto* mission = new Mission(this, missionId); - - mission->LoadFromXmlDone(*doneM); - - doneM = doneM->NextSiblingElement(); - - m_Missions.insert_or_assign(missionId, mission); - } - - auto* currentM = cur->FirstChildElement(); - - uint32_t missionOrder{}; - while (currentM) { - int missionId; - - currentM->QueryAttribute("id", &missionId); - - auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId); - - mission->LoadFromXmlCur(*currentM); - - if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) { - mission->SetUniqueMissionOrderID(missionOrder); - if (missionOrder > m_LastUsedMissionOrderUID) m_LastUsedMissionOrderUID = missionOrder; + if (done) { + auto* doneM = done->FirstChildElement(); + + while (doneM) { + int missionId; + + doneM->QueryAttribute("id", &missionId); + + auto* mission = new Mission(this, missionId); + + mission->LoadFromXmlDone(*doneM); + + doneM = doneM->NextSiblingElement(); + + m_Missions.insert_or_assign(missionId, mission); } + } + + auto* cur = mis->FirstChildElement("cur"); + if (cur) { + auto* currentM = cur->FirstChildElement(); - currentM = currentM->NextSiblingElement(); + uint32_t missionOrder{}; + while (currentM) { + int missionId; - m_Missions.insert_or_assign(missionId, mission); + currentM->QueryAttribute("id", &missionId); + + auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId); + + mission->LoadFromXmlCur(*currentM); + + if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) { + mission->SetUniqueMissionOrderID(missionOrder); + if (missionOrder > m_LastUsedMissionOrderUID) m_LastUsedMissionOrderUID = missionOrder; + } + + currentM = currentM->NextSiblingElement(); + + m_Missions.insert_or_assign(missionId, mission); + } } } diff --git a/dGame/dComponents/MultiZoneEntranceComponent.cpp b/dGame/dComponents/MultiZoneEntranceComponent.cpp index f8dabf18..674af099 100644 --- a/dGame/dComponents/MultiZoneEntranceComponent.cpp +++ b/dGame/dComponents/MultiZoneEntranceComponent.cpp @@ -17,7 +17,10 @@ MultiZoneEntranceComponent::MultiZoneEntranceComponent(Entity* parent, const int MultiZoneEntranceComponent::~MultiZoneEntranceComponent() {} void MultiZoneEntranceComponent::OnUse(Entity* originator) { - auto* rocket = originator->GetComponent<CharacterComponent>()->RocketEquip(originator); + auto* const characterComponent = originator->GetComponent<CharacterComponent>(); + if (!characterComponent) return; + + auto* rocket = characterComponent->RocketEquip(originator); if (!rocket) return; // the LUP world menu is just the property menu, the client knows how to handle it @@ -26,7 +29,7 @@ void MultiZoneEntranceComponent::OnUse(Entity* originator) { void MultiZoneEntranceComponent::OnSelectWorld(Entity* originator, uint32_t index) { auto* rocketLaunchpadControlComponent = m_Parent->GetComponent<RocketLaunchpadControlComponent>(); - if (!rocketLaunchpadControlComponent) return; + if (!rocketLaunchpadControlComponent || index >= m_LUPWorlds.size()) return; rocketLaunchpadControlComponent->Launch(originator, m_LUPWorlds[index], 0); } diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index 6fb5eedf..cd2b50b9 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -922,7 +922,9 @@ void PetComponent::Deactivate() { } void PetComponent::Release() { - auto* inventoryComponent = GetOwner()->GetComponent<InventoryComponent>(); + auto* const owner = GetOwner(); + if (!owner) return; + auto* const inventoryComponent = owner->GetComponent<InventoryComponent>(); if (inventoryComponent == nullptr) { return; @@ -932,9 +934,9 @@ void PetComponent::Release() { inventoryComponent->RemoveDatabasePet(m_DatabaseId); - auto* item = inventoryComponent->FindItemBySubKey(m_DatabaseId); + auto* const item = inventoryComponent->FindItemBySubKey(m_DatabaseId); - item->SetCount(0, false, false); + if (item) item->SetCount(0, false, false); } void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, const int32_t commandType, const int32_t typeId, const bool overrideObey) { diff --git a/dGame/dComponents/PhantomPhysicsComponent.cpp b/dGame/dComponents/PhantomPhysicsComponent.cpp index 01ef7dfd..a65df484 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.cpp +++ b/dGame/dComponents/PhantomPhysicsComponent.cpp @@ -58,31 +58,16 @@ PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, const int32_t c } if (m_IsRespawnVolume) { - { - auto respawnString = std::stringstream(m_Parent->GetVarAsString(u"rspPos")); + const auto respawnPos = GeneralUtils::SplitString(m_Parent->GetVarAsString(u"rspPos"), '\x1f'); + m_RespawnPos = GeneralUtils::TryParse(respawnPos, NiPoint3Constant::ZERO); - std::string segment; - std::vector<std::string> seglist; - - while (std::getline(respawnString, segment, '\x1f')) { - seglist.push_back(segment); - } - - m_RespawnPos = NiPoint3(std::stof(seglist[0]), std::stof(seglist[1]), std::stof(seglist[2])); - } - - { - auto respawnString = std::stringstream(m_Parent->GetVarAsString(u"rspRot")); - - std::string segment; - std::vector<std::string> seglist; - - while (std::getline(respawnString, segment, '\x1f')) { - seglist.push_back(segment); - } - - m_RespawnRot = NiQuaternion(std::stof(seglist[0]), std::stof(seglist[1]), std::stof(seglist[2]), std::stof(seglist[3])); - } + const auto respawnRot = GeneralUtils::SplitString(m_Parent->GetVarAsString(u"rspRot"), '\x1f'); + m_RespawnRot = respawnRot.size() >= 4 ? NiQuaternion( + GeneralUtils::TryParse(respawnRot[0], 1.0f), + GeneralUtils::TryParse(respawnRot[1], 0.0f), + GeneralUtils::TryParse(respawnRot[2], 0.0f), + GeneralUtils::TryParse(respawnRot[3], 0.0f)) + : QuatUtils::IDENTITY; } // HF - RespawnPoints. Legacy respawn entity. diff --git a/dGame/dComponents/PropertyEntranceComponent.cpp b/dGame/dComponents/PropertyEntranceComponent.cpp index 1f3b28a2..f282a73d 100644 --- a/dGame/dComponents/PropertyEntranceComponent.cpp +++ b/dGame/dComponents/PropertyEntranceComponent.cpp @@ -131,7 +131,7 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl const auto lookupResult = Database::Get()->GetProperties(propertyLookup); - for (const auto& propertyEntry : lookupResult->entries) { + for (const auto& propertyEntry : lookupResult.entries) { const auto owner = propertyEntry.ownerId; const auto otherCharacter = Database::Get()->GetCharacterInfo(owner); if (!otherCharacter.has_value()) { @@ -174,5 +174,5 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl } // Query here is to figure out whether or not to display the button to go to the next page or not. - GameMessages::SendPropertySelectQuery(m_Parent->GetObjectID(), startIndex, lookupResult->totalEntriesMatchingQuery - (startIndex + numResults) > 0, character->GetPropertyCloneID(), false, true, entries, sysAddr); + GameMessages::SendPropertySelectQuery(m_Parent->GetObjectID(), startIndex, lookupResult.totalEntriesMatchingQuery - (startIndex + numResults) > 0, character->GetPropertyCloneID(), false, true, entries, sysAddr); } diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 59b918e6..3c62dc52 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -107,20 +107,12 @@ std::vector<NiPoint3> PropertyManagementComponent::GetPaths() const { std::vector<float> points; - std::istringstream stream(result.getStringField("path")); - std::string token; - - while (std::getline(stream, token, ' ')) { - try { - auto value = std::stof(token); - - points.push_back(value); - } catch (std::invalid_argument& exception) { - LOG("Failed to parse value (%s): (%s)!", token.c_str(), exception.what()); - } + for (const auto& str : GeneralUtils::SplitString(result.getStringField("path"), ' ')) { + const auto value = GeneralUtils::TryParse<float>(str); + if (value) points.push_back(value.value()); } - for (auto i = 0u; i < points.size(); i += 3) { + for (auto i = 0u; i + 2 < points.size(); i += 3) { paths.emplace_back(points[i], points[i + 1], points[i + 2]); } @@ -780,15 +772,17 @@ void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const privacy = static_cast<char>(this->privacyOption); if (moderatorRequested) { auto moderationInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); - if (moderationInfo->rejectionReason != "") { - moderatorRequested = false; - rejectionReason = moderationInfo->rejectionReason; - } else if (moderationInfo->rejectionReason == "" && moderationInfo->modApproved == 1) { - moderatorRequested = false; - rejectionReason = ""; - } else { - moderatorRequested = true; - rejectionReason = ""; + if (moderationInfo) { + if (moderationInfo->rejectionReason != "") { + moderatorRequested = false; + rejectionReason = moderationInfo->rejectionReason; + } else if (moderationInfo->rejectionReason == "" && moderationInfo->modApproved == 1) { + moderatorRequested = false; + rejectionReason = ""; + } else { + moderatorRequested = true; + rejectionReason = ""; + } } } } diff --git a/dGame/dComponents/QuickBuildComponent.cpp b/dGame/dComponents/QuickBuildComponent.cpp index 4f167c17..0940d19d 100644 --- a/dGame/dComponents/QuickBuildComponent.cpp +++ b/dGame/dComponents/QuickBuildComponent.cpp @@ -380,7 +380,7 @@ void QuickBuildComponent::StartQuickBuild(Entity* const user) { m_Builder = user->GetObjectID(); auto* character = user->GetComponent<CharacterComponent>(); - character->SetCurrentActivity(eGameActivity::QUICKBUILDING); + if (character) character->SetCurrentActivity(eGameActivity::QUICKBUILDING); Game::entityManager->SerializeEntity(user); diff --git a/dGame/dComponents/RacingControlComponent.cpp b/dGame/dComponents/RacingControlComponent.cpp index 6d0f675b..06cd02be 100644 --- a/dGame/dComponents/RacingControlComponent.cpp +++ b/dGame/dComponents/RacingControlComponent.cpp @@ -306,7 +306,7 @@ void RacingControlComponent::OnRequestDie(Entity* player, const std::u16string& eKillType::VIOLENT, deathType, 0, 0, 90.0f, false, true, 0); auto* destroyableComponent = vehicle->GetComponent<DestroyableComponent>(); - uint32_t respawnImagination = 0; + int32_t respawnImagination = 0; // Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live. // Do not actually change the value yet. Do that on respawn. if (destroyableComponent) { diff --git a/dGame/dComponents/TriggerComponent.cpp b/dGame/dComponents/TriggerComponent.cpp index e0bd1c4c..19384938 100644 --- a/dGame/dComponents/TriggerComponent.cpp +++ b/dGame/dComponents/TriggerComponent.cpp @@ -385,26 +385,30 @@ void TriggerComponent::HandleSetPhysicsVolumeEffect(Entity* targetEntity, std::v } phantomPhysicsComponent->SetPhysicsEffectActive(true); ePhysicsEffectType effectType = ePhysicsEffectType::PUSH; - std::transform(argArray.at(0).begin(), argArray.at(0).end(), argArray.at(0).begin(), ::tolower); //Transform to lowercase - if (argArray.at(0) == "push") effectType = ePhysicsEffectType::PUSH; - else if (argArray.at(0) == "attract") effectType = ePhysicsEffectType::ATTRACT; - else if (argArray.at(0) == "repulse") effectType = ePhysicsEffectType::REPULSE; - else if (argArray.at(0) == "gravity") effectType = ePhysicsEffectType::GRAVITY_SCALE; - else if (argArray.at(0) == "friction") effectType = ePhysicsEffectType::FRICTION; + if (!argArray.empty()) { + std::transform(argArray.at(0).begin(), argArray.at(0).end(), argArray.at(0).begin(), ::tolower); //Transform to lowercase + if (argArray.at(0) == "push") effectType = ePhysicsEffectType::PUSH; + else if (argArray.at(0) == "attract") effectType = ePhysicsEffectType::ATTRACT; + else if (argArray.at(0) == "repulse") effectType = ePhysicsEffectType::REPULSE; + else if (argArray.at(0) == "gravity") effectType = ePhysicsEffectType::GRAVITY_SCALE; + else if (argArray.at(0) == "friction") effectType = ePhysicsEffectType::FRICTION; + } phantomPhysicsComponent->SetEffectType(effectType); - phantomPhysicsComponent->SetDirectionalMultiplier(std::stof(argArray.at(1))); + if (argArray.size() > 1) { + phantomPhysicsComponent->SetDirectionalMultiplier(GeneralUtils::TryParse(argArray.at(1), 0.0f)); + } if (argArray.size() > 4) { const NiPoint3 direction = - GeneralUtils::TryParse<NiPoint3>(argArray.at(2), argArray.at(3), argArray.at(4)).value_or(NiPoint3Constant::ZERO); + GeneralUtils::TryParse(argArray.at(2), argArray.at(3), argArray.at(4), NiPoint3Constant::ZERO); phantomPhysicsComponent->SetDirection(direction); } - if (argArray.size() > 5) { - const uint32_t min = GeneralUtils::TryParse<uint32_t>(argArray.at(6)).value_or(0); + if (argArray.size() > 6) { + const uint32_t min = GeneralUtils::TryParse(argArray.at(6), 0); phantomPhysicsComponent->SetMin(min); - const uint32_t max = GeneralUtils::TryParse<uint32_t>(argArray.at(7)).value_or(0); + const uint32_t max = GeneralUtils::TryParse(argArray.at(7), 0); phantomPhysicsComponent->SetMax(max); } diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 1e89e2a9..e7aa97f9 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -61,6 +61,11 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System User* usr = UserManager::Instance()->GetUser(sysAddr); + if (!usr) { + LOG("Failed to find a logged in user for (%llu), aborting GM: %4i, %s!", objectID, messageID, StringifiedEnum::ToString(messageID).data()); + return; + } + if (!entity) { LOG("Failed to find associated entity (%llu), aborting GM: %4i, %s!", objectID, messageID, StringifiedEnum::ToString(messageID).data()); return; @@ -76,7 +81,8 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System if (msg->requiredGmLevel > eGameMasterLevel::CIVILIAN) { auto* usingEntity = Game::entityManager->GetEntity(usr->GetLoggedInChar()); if (!usingEntity || usingEntity->GetGMLevel() < msg->requiredGmLevel) { - LOG("User %s (%llu) does not have the required GM level to execute this command.", usingEntity->GetCharacter()->GetName().c_str(), usingEntity->GetObjectID()); + if (usingEntity) LOG("User %s (%llu) does not have the required GM level to execute this command.", usingEntity->GetCharacter()->GetName().c_str(), usingEntity->GetObjectID()); + else LOG("ObjectID %llu tried to use a gm required message.", usr->GetLoggedInChar()); return; } } @@ -167,8 +173,8 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::SendRestoreToPostLoadStats(entity, sysAddr); - auto* destroyable = entity->GetComponent<DestroyableComponent>(); - destroyable->SetImagination(destroyable->GetImagination()); + auto* const destroyable = entity->GetComponent<DestroyableComponent>(); + if (destroyable) destroyable->SetImagination(destroyable->GetImagination()); Game::entityManager->SerializeEntity(entity); std::vector<Entity*> racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); @@ -186,7 +192,7 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System std::vector<Entity*> scriptedActs = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::SCRIPT); for (Entity* scriptEntity : scriptedActs) { - if (scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds + if (!zoneControl || scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds scriptEntity->GetScript()->OnPlayerLoaded(scriptEntity, entity); } } @@ -332,9 +338,9 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System if (behaviorId > 0) { auto bs = RakNet::BitStream(reinterpret_cast<unsigned char*>(&startSkill.sBitStream[0]), startSkill.sBitStream.size(), false); - auto* skillComponent = entity->GetComponent<SkillComponent>(); + auto* const skillComponent = entity->GetComponent<SkillComponent>(); - success = skillComponent->CastPlayerSkill(behaviorId, startSkill.uiSkillHandle, bs, startSkill.optionalTargetID, startSkill.skillID); + if (skillComponent) success = skillComponent->CastPlayerSkill(behaviorId, startSkill.uiSkillHandle, bs, startSkill.optionalTargetID, startSkill.skillID); if (success && entity->GetCharacter()) { DestroyableComponent* destComp = entity->GetComponent<DestroyableComponent>(); @@ -387,9 +393,9 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System if (usr != nullptr) { auto bs = RakNet::BitStream(reinterpret_cast<unsigned char*>(&sync.sBitStream[0]), sync.sBitStream.size(), false); - auto* skillComponent = entity->GetComponent<SkillComponent>(); + auto* const skillComponent = entity->GetComponent<SkillComponent>(); - skillComponent->SyncPlayerSkill(sync.uiSkillHandle, sync.uiBehaviorHandle, bs); + if (skillComponent) skillComponent->SyncPlayerSkill(sync.uiSkillHandle, sync.uiBehaviorHandle, bs); } EchoSyncSkill echo = EchoSyncSkill(); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 8f4fdeb4..3af6cb48 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -2137,6 +2137,7 @@ void GameMessages::HandleUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& inStream.Read(worldId); inStream.Read(descriptionLength); + if (descriptionLength > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < descriptionLength; ++i) { uint16_t character; inStream.Read(character); @@ -2144,6 +2145,7 @@ void GameMessages::HandleUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& } inStream.Read(nameLength); + if (nameLength > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < nameLength; ++i) { uint16_t character; inStream.Read(character); @@ -2474,9 +2476,15 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent uint32_t sd0Size; inStream.Read(sd0Size); - std::unique_ptr<char[]> sd0Data(new char[sd0Size]); - if (sd0Data == nullptr) return; + // For the sake of letting players make models as big as they want, only reject if we cant allocate the required memory. + std::unique_ptr<char[]> sd0Data; + try { + sd0Data.reset(new char[sd0Size]); + } catch (std::exception& e) { + LOG("Failed to allocate sd0 of size %u", sd0Size); + return; + } inStream.ReadAlignedBytes(reinterpret_cast<unsigned char*>(sd0Data.get()), sd0Size); @@ -2652,6 +2660,7 @@ void GameMessages::HandlePropertyEntranceSync(RakNet::BitStream& inStream, Entit inStream.Read(startIndex); inStream.Read(filterTextLength); + if (filterTextLength > MAX_MESSAGE_LENGTH) return; for (auto i = 0u; i < filterTextLength; i++) { char c; inStream.Read(c); @@ -3040,6 +3049,7 @@ void GameMessages::HandleVerifyAck(RakNet::BitStream& inStream, Entity* entity, uint32_t sBitStreamLength = 0; inStream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return; for (uint64_t k = 0; k < sBitStreamLength; k++) { uint8_t character; inStream.Read(character); @@ -3261,6 +3271,7 @@ void GameMessages::HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* inStream.Read(currency); inStream.Read(itemCount); + if (itemCount > MAX_MESSAGE_LENGTH) return; LOG("Trade update from (%llu) -> (%llu), (%i)", entity->GetObjectID(), currency, itemCount); @@ -3673,6 +3684,7 @@ void GameMessages::HandleRequestSetPetName(RakNet::BitStream& inStream, Entity* inStream.Read(nameLength); + if (nameLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < nameLength; i++) { char16_t character; inStream.Read(character); @@ -3742,6 +3754,7 @@ void GameMessages::HandleMessageBoxResponse(RakNet::BitStream& inStream, Entity* inStream.Read(iButton); inStream.Read(identifierLength); + if (identifierLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < identifierLength; i++) { char16_t character; inStream.Read(character); @@ -3749,6 +3762,7 @@ void GameMessages::HandleMessageBoxResponse(RakNet::BitStream& inStream, Entity* } inStream.Read(userDataLength); + if (userDataLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < userDataLength; i++) { char16_t character; inStream.Read(character); @@ -3796,6 +3810,7 @@ void GameMessages::HandleChoiceBoxRespond(RakNet::BitStream& inStream, Entity* e std::u16string identifier; inStream.Read(buttonIdentifierLength); + if (buttonIdentifierLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < buttonIdentifierLength; i++) { char16_t character; inStream.Read(character); @@ -3805,6 +3820,7 @@ void GameMessages::HandleChoiceBoxRespond(RakNet::BitStream& inStream, Entity* e inStream.Read(iButton); inStream.Read(identifierLength); + if (identifierLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < identifierLength; i++) { char16_t character; inStream.Read(character); @@ -4158,7 +4174,13 @@ void GameMessages::HandleUpdatePropertyPerformanceCost(RakNet::BitStream& inStre return; } - Database::Get()->UpdatePerformanceCost(zone->GetZoneID(), performanceCost); + const auto* const propertyManagementComponent = entity->GetComponent<PropertyManagementComponent>(); + const auto* const ownerEntity = propertyManagementComponent ? propertyManagementComponent->GetOwner() : nullptr; + const auto* const character = ownerEntity ? ownerEntity->GetCharacter() : nullptr; + const auto& zoneID = zone->GetZoneID(); + if (character && character->GetPropertyCloneID() == zoneID.GetCloneID()) { + Database::Get()->UpdatePerformanceCost(zoneID, performanceCost); + } } void GameMessages::HandleVehicleNotifyHitImaginationServer(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { @@ -4783,13 +4805,19 @@ void GameMessages::HandleParseChatMessage(RakNet::BitStream& inStream, Entity* e uint32_t wsStringLength; inStream.Read(wsStringLength); + + if (wsStringLength > MAX_MESSAGE_LENGTH) { + LOG("Max message length reached, capping message."); + wsStringLength = MAX_MESSAGE_LENGTH; + } + for (uint32_t i = 0; i < wsStringLength; ++i) { uint16_t character; inStream.Read(character); wsString.push_back(character); } - if (wsString[0] == L'/') { + if (!wsString.empty() && wsString[0] == L'/') { SlashCommandHandler::HandleChatCommand(wsString, entity, sysAddr); } } @@ -4806,6 +4834,7 @@ void GameMessages::HandleFireEventServerSide(RakNet::BitStream& inStream, Entity LWOOBJID senderID{}; inStream.Read(argsLength); + if (argsLength > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < argsLength; ++i) { uint16_t character; inStream.Read(character); @@ -5436,7 +5465,7 @@ void GameMessages::HandleModularBuildFinish(RakNet::BitStream& inStream, Entity* std::vector<LOT> modList; auto& oldPartList = character->GetVar<std::string>(u"currentModifiedBuild"); bool everyPieceSwapped = !oldPartList.empty(); // If the player didn't put a build in initially, then they should not get this achievement. - if (count >= 3) { + if (count >= 3 && count < 8) { std::u16string modules; for (uint32_t k = 0; k < count; k++) { @@ -5733,6 +5762,7 @@ void GameMessages::HandleMatchRequest(RakNet::BitStream& inStream, Entity* entit inStream.Read(activator); inStream.Read(playerChoicesLen); + if (playerChoicesLen > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < playerChoicesLen; ++i) { uint16_t character; inStream.Read(character); @@ -5890,7 +5920,7 @@ void GameMessages::HandlePlayerRailArrivedNotification(RakNet::BitStream& inStre const SystemAddress& sysAddr) { uint32_t pathNameLength; inStream.Read(pathNameLength); - + if (pathNameLength > MAX_MESSAGE_LENGTH) return; std::u16string pathName; for (auto k = 0; k < pathNameLength; k++) { uint16_t c; diff --git a/dGame/dInventory/Inventory.cpp b/dGame/dInventory/Inventory.cpp index db83ac58..f5742165 100644 --- a/dGame/dInventory/Inventory.cpp +++ b/dGame/dInventory/Inventory.cpp @@ -75,7 +75,8 @@ uint32_t Inventory::GetLotCount(const LOT lot) const { } void Inventory::SetSize(const uint32_t value) { - free += static_cast<int32_t>(value) - static_cast<int32_t>(size); + const auto delta = static_cast<int32_t>(value) - static_cast<int32_t>(size); + free = static_cast<uint32_t>(std::max(0, static_cast<int32_t>(free) + delta)); size = value; diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 7ee27f01..19b536a4 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -401,7 +401,8 @@ void Item::Disassemble(const eInventoryType inventoryType) { const auto deliminator = '+'; while (std::getline(ssData, token, deliminator)) { - const auto modLot = std::stoi(token.substr(2, token.size() - 1)); + if (token.size() <= 2) continue; // invalid token, must have size of at least 3. + const auto modLot = GeneralUtils::TryParse(token.substr(2, token.size() - 1), LOT_NULL); modArray.push_back(modLot); } @@ -440,7 +441,10 @@ void Item::DisassembleModel(uint32_t numToDismantle) { std::vector<std::string> renderAssetSplit = GeneralUtils::SplitString(renderAsset, '/'); if (renderAssetSplit.empty()) return; - std::string lxfmlPath = "BrickModels" + lxfmlFolderName + "/" + GeneralUtils::SplitString(renderAssetSplit.back(), '.').at(0) + ".lxfml"; + const auto renderAssetSplitSplit = GeneralUtils::SplitString(renderAssetSplit.back(), '.'); + if (renderAssetSplitSplit.empty()) return; + + std::string lxfmlPath = "BrickModels" + lxfmlFolderName + "/" + renderAssetSplitSplit[0] + ".lxfml"; auto file = Game::assetManager->GetFile(lxfmlPath.c_str()); if (!file) { diff --git a/dGame/dInventory/ItemSet.cpp b/dGame/dInventory/ItemSet.cpp index d8d320ed..1544e611 100644 --- a/dGame/dInventory/ItemSet.cpp +++ b/dGame/dInventory/ItemSet.cpp @@ -128,8 +128,8 @@ void ItemSet::OnEquip(const LOT lot) { return; } - auto* skillComponent = m_InventoryComponent->GetParent()->GetComponent<SkillComponent>(); - auto* missionComponent = m_InventoryComponent->GetParent()->GetComponent<MissionComponent>(); + auto [skillComponent, missionComponent] = m_InventoryComponent->GetParent()->GetComponentsMut<SkillComponent, MissionComponent>(); + if (!skillComponent || !missionComponent) return; // Nothing to do here if these are null for (const auto skill : skillSet) { auto* skillTable = CDClientManager::GetTable<CDSkillBehaviorTable>(); diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index e13cc411..4afb2207 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -74,23 +74,19 @@ Mission::Mission(MissionComponent* missionComponent, const uint32_t missionId) { void Mission::LoadFromXmlDone(const tinyxml2::XMLElement& element) { // Start custom XML - if (element.Attribute("state") != nullptr) { - m_State = static_cast<eMissionState>(std::stoul(element.Attribute("state"))); - } + m_State = static_cast<eMissionState>(element.UnsignedAttribute("state")); // End custom XML - if (element.Attribute("cct") != nullptr) { - m_Completions = std::stoul(element.Attribute("cct")); + m_Completions = element.UnsignedAttribute("cct"); - m_Timestamp = std::stoul(element.Attribute("cts")); - } + m_Timestamp = element.UnsignedAttribute("cts"); } void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) { const auto* const character = GetCharacter(); // Start custom XML if (element.Attribute("state") != nullptr) { - m_State = static_cast<eMissionState>(std::stoul(element.Attribute("state"))); + m_State = static_cast<eMissionState>(element.IntAttribute("state", -1)); } // End custom XML @@ -106,7 +102,7 @@ void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) { const auto type = curTask->GetType(); - auto value = std::stoul(task->Attribute("v")); + auto value = task->UnsignedAttribute("v"); curTask->SetProgress(value, false); task = task->NextSiblingElement(); @@ -114,7 +110,7 @@ void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) { if (type == eMissionTaskType::COLLECTION || type == eMissionTaskType::VISIT_PROPERTY) { std::vector<uint32_t> uniques; while (task != nullptr && value > 0) { - const auto unique = std::stoul(task->Attribute("v")); + const auto unique = task->UnsignedAttribute("v"); uniques.push_back(unique); diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index 139b6d04..c19c4d2f 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -17,50 +17,63 @@ PropertyBehavior::PropertyBehavior(bool _isTemplated) { isTemplated = _isTemplated; } +bool CheckStateRange(const BehaviorState state) { + return state >= BehaviorState::HOME_STATE && state <= BehaviorState::STAR_STATE; +} + template<> void PropertyBehavior::HandleMsg(AddStripMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(AddActionMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(RearrangeStripMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(UpdateActionMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(UpdateStripUiMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(RemoveStripMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(RemoveActionsMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(SplitStripMessage& msg) { + if (!CheckStateRange(msg.GetSourceActionContext().GetStateId())) return; + if (!CheckStateRange(msg.GetDestinationActionContext().GetStateId())) return; m_States[msg.GetSourceActionContext().GetStateId()].HandleMsg(msg); m_States[msg.GetDestinationActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetDestinationActionContext().GetStateId(); @@ -68,6 +81,8 @@ void PropertyBehavior::HandleMsg(SplitStripMessage& msg) { template<> void PropertyBehavior::HandleMsg(MigrateActionsMessage& msg) { + if (!CheckStateRange(msg.GetSourceActionContext().GetStateId())) return; + if (!CheckStateRange(msg.GetDestinationActionContext().GetStateId())) return; m_States[msg.GetSourceActionContext().GetStateId()].HandleMsg(msg); m_States[msg.GetDestinationActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetDestinationActionContext().GetStateId(); @@ -75,6 +90,8 @@ void PropertyBehavior::HandleMsg(MigrateActionsMessage& msg) { template<> void PropertyBehavior::HandleMsg(MergeStripsMessage& msg) { + if (!CheckStateRange(msg.GetSourceActionContext().GetStateId())) return; + if (!CheckStateRange(msg.GetDestinationActionContext().GetStateId())) return; m_States[msg.GetSourceActionContext().GetStateId()].HandleMsg(msg); m_States[msg.GetDestinationActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetDestinationActionContext().GetStateId(); diff --git a/dGame/dUtilities/BrickDatabase.cpp b/dGame/dUtilities/BrickDatabase.cpp index 61a7341d..2cc034e9 100644 --- a/dGame/dUtilities/BrickDatabase.cpp +++ b/dGame/dUtilities/BrickDatabase.cpp @@ -66,13 +66,9 @@ const BrickList& BrickDatabase::GetBricks(const LxfmlPath& lxfmlPath) { std::string materialString(materialList); const auto materials = GeneralUtils::SplitString(materialString, ','); - if (!materials.empty()) { - brick.materialID = std::stoi(materials[0]); - } else { - brick.materialID = 0; - } + brick.materialID = GeneralUtils::TryParse(materials[0], 0); } else if (materialID != nullptr) { - brick.materialID = std::stoi(materialID); + brick.materialID = GeneralUtils::TryParse(materialID, 0); } else { brick.materialID = 0; // This is bad, makes it so the minigame can't be played } diff --git a/dGame/dUtilities/CheatDetection.cpp b/dGame/dUtilities/CheatDetection.cpp index a0f8c53a..acc415a6 100644 --- a/dGame/dUtilities/CheatDetection.cpp +++ b/dGame/dUtilities/CheatDetection.cpp @@ -54,17 +54,23 @@ void LogAndSaveFailedAntiCheatCheck(const LWOOBJID& id, const SystemAddress& sys // If player exists and entity exists in world, use both for logging info. if (entity && player) { + const auto* const playerChar = player->GetCharacter(); + const auto& playerName = playerChar ? playerChar->GetName() : "(null player character)"; + const auto* const entityChar = entity->GetCharacter(); + const auto& entityName = entityChar ? entityChar->GetName() : "(null entity character)"; LOG("Player (%s) (%llu) at system address (%s) with sending player (%s) (%llu) does not match their own.", - player->GetCharacter()->GetName().c_str(), player->GetObjectID(), + playerName.c_str(), player->GetObjectID(), sysAddr.ToString(), - entity->GetCharacter()->GetName().c_str(), entity->GetObjectID()); - if (player->GetCharacter()) toReport = player->GetCharacter()->GetParentUser(); + entityName.c_str(), entity->GetObjectID()); + if (playerChar) toReport = playerChar->GetParentUser(); // In the case that the target entity id did not exist, just log the player info. } else if (player) { + const auto* const playerChar = player->GetCharacter(); + const auto& playerName = playerChar ? playerChar->GetName() : "(null player character)"; LOG("Player (%s) (%llu) at system address (%s) with sending player (%llu) does not match their own.", - player->GetCharacter()->GetName().c_str(), player->GetObjectID(), + playerName.c_str(), player->GetObjectID(), sysAddr.ToString(), id); - if (player->GetCharacter()) toReport = player->GetCharacter()->GetParentUser(); + if (playerChar) toReport = playerChar->GetParentUser(); // In the rare case that the player does not exist, just log the system address and who the target id was. } else { LOG("Player at system address (%s) with sending player (%llu) does not match their own.", @@ -76,8 +82,11 @@ void LogAndSaveFailedAntiCheatCheck(const LWOOBJID& id, const SystemAddress& sys auto* user = UserManager::Instance()->GetUser(sysAddr); if (user) { + const auto* const lastChar = user->GetLastUsedChar(); + const auto& lastName = lastChar ? lastChar->GetName() : "(null last char)"; + const auto lastObjID = lastChar ? lastChar->GetObjectID() : LWOOBJID_EMPTY; LOG("User at system address (%s) (%s) (%llu) sent a packet as (%llu) which is not an id they own.", - sysAddr.ToString(), user->GetLastUsedChar()->GetName().c_str(), user->GetLastUsedChar()->GetObjectID(), id); + sysAddr.ToString(), lastName.c_str(), lastObjID, id); // Can't know sending player. Just log system address for IP banning. } else { LOG("No user found for system address (%s).", sysAddr.ToString()); diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index 2fe7b796..2edec1c4 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -326,7 +326,7 @@ void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg, const bo void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDropInfo>& rolledItems, uint32_t minCoins, uint32_t maxCoins, const bool noTeamLootOnDeath) { player = player->GetOwner(); // if the owner is overwritten, we collect that here const auto playerID = player->GetObjectID(); - if (!player || !player->IsPlayer()) { + if (!player->IsPlayer()) { LOG("Trying to drop loot for non-player %llu:%i", playerID, player->GetLOT()); return; } diff --git a/dGame/dUtilities/Mail.cpp b/dGame/dUtilities/Mail.cpp index 5f1377e7..b0054513 100644 --- a/dGame/dUtilities/Mail.cpp +++ b/dGame/dUtilities/Mail.cpp @@ -74,69 +74,74 @@ namespace Mail { void SendRequest::Handle() { SendResponse response; auto* character = player->GetCharacter(); - const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted(); - const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess); - - if (character && !(restrictedMailAccess || restrictMailOnMute)) { - mailInfo.recipient = std::regex_replace(mailInfo.recipient, std::regex("[^0-9a-zA-Z]+"), ""); - auto receiverID = Database::Get()->GetCharacterInfo(mailInfo.recipient); - - if (!receiverID) { - response.status = eSendResponse::RecipientNotFound; - } else if (GeneralUtils::CaseInsensitiveStringCompare(mailInfo.recipient, character->GetName()) || receiverID->id == character->GetID()) { - response.status = eSendResponse::CannotMailSelf; - } else { - uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee; - uint32_t stackSize = 0; - - auto inventoryComponent = player->GetComponent<InventoryComponent>(); - Item* item = nullptr; - - bool hasAttachment = mailInfo.itemID != 0 && mailInfo.itemCount > 0; - - if (hasAttachment) { - item = inventoryComponent->FindItemById(mailInfo.itemID); - if (item) { - mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig().mailPercentAttachmentFee); - mailInfo.itemLOT = item->GetLot(); - } - } - - if (hasAttachment && !item) { - response.status = eSendResponse::AttachmentNotFound; - } else if (player->GetCharacter()->GetCoins() - mailCost < 0) { - response.status = eSendResponse::NotEnoughCoins; - } else { - bool removeSuccess = true; - // Remove coins and items from the sender - player->GetCharacter()->SetCoins(player->GetCharacter()->GetCoins() - mailCost, eLootSourceType::MAIL); - if (inventoryComponent && hasAttachment && item) { - removeSuccess = inventoryComponent->RemoveItem(mailInfo.itemLOT, mailInfo.itemCount, ALL, true); - auto* missionComponent = player->GetComponent<MissionComponent>(); - if (missionComponent && removeSuccess) missionComponent->Progress(eMissionTaskType::GATHER, mailInfo.itemLOT, LWOOBJID_EMPTY, "", -mailInfo.itemCount); - } - - // we passed all the checks, now we can actully send the mail - if (removeSuccess) { - mailInfo.senderId = character->GetID(); - mailInfo.senderUsername = character->GetName(); - mailInfo.receiverId = receiverID->id; - mailInfo.itemSubkey = LWOOBJID_EMPTY; - - //clear out the attachementID - mailInfo.itemID = 0; - - Database::Get()->InsertNewMail(mailInfo); - response.status = eSendResponse::Success; - character->SaveXMLToDatabase(); - } else { - response.status = eSendResponse::AttachmentNotFound; - } - } - } + if (!character) { + response.status = eSendResponse::UnknownError; } else { - response.status = eSendResponse::SenderAccountIsMuted; + const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted(); + const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess); + + if (character && !(restrictedMailAccess || restrictMailOnMute)) { + mailInfo.recipient = std::regex_replace(mailInfo.recipient, std::regex("[^0-9a-zA-Z]+"), ""); + auto receiverID = Database::Get()->GetCharacterInfo(mailInfo.recipient); + + if (!receiverID) { + response.status = eSendResponse::RecipientNotFound; + } else if (GeneralUtils::CaseInsensitiveStringCompare(mailInfo.recipient, character->GetName()) || receiverID->id == character->GetID()) { + response.status = eSendResponse::CannotMailSelf; + } else { + uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee; + uint32_t stackSize = 0; + + auto inventoryComponent = player->GetComponent<InventoryComponent>(); + Item* item = nullptr; + + bool hasAttachment = mailInfo.itemID != 0 && mailInfo.itemCount > 0; + + if (hasAttachment) { + item = inventoryComponent->FindItemById(mailInfo.itemID); + if (item) { + mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig().mailPercentAttachmentFee); + mailInfo.itemLOT = item->GetLot(); + } + } + + if (hasAttachment && !item) { + response.status = eSendResponse::AttachmentNotFound; + } else if (player->GetCharacter()->GetCoins() - mailCost < 0) { + response.status = eSendResponse::NotEnoughCoins; + } else { + bool removeSuccess = true; + // Remove coins and items from the sender + player->GetCharacter()->SetCoins(player->GetCharacter()->GetCoins() - mailCost, eLootSourceType::MAIL); + if (inventoryComponent && hasAttachment && item) { + removeSuccess = inventoryComponent->RemoveItem(mailInfo.itemLOT, mailInfo.itemCount, ALL, true); + auto* missionComponent = player->GetComponent<MissionComponent>(); + if (missionComponent && removeSuccess) missionComponent->Progress(eMissionTaskType::GATHER, mailInfo.itemLOT, LWOOBJID_EMPTY, "", -mailInfo.itemCount); + } + + // we passed all the checks, now we can actully send the mail + if (removeSuccess) { + mailInfo.senderId = character->GetID(); + mailInfo.senderUsername = character->GetName(); + mailInfo.receiverId = receiverID->id; + mailInfo.itemSubkey = LWOOBJID_EMPTY; + + //clear out the attachementID + mailInfo.itemID = 0; + + Database::Get()->InsertNewMail(mailInfo); + response.status = eSendResponse::Success; + character->SaveXMLToDatabase(); + } else { + response.status = eSendResponse::AttachmentNotFound; + } + } + } + } else { + response.status = eSendResponse::SenderAccountIsMuted; + } } + LOG("Finished send with status %s", StringifiedEnum::ToString(response.status).data()); response.Send(sysAddr); } @@ -193,7 +198,7 @@ namespace Mail { if (mailID > 0 && playerID == player->GetObjectID() && inv) { auto playerMail = Database::Get()->GetMail(mailID); - if (!playerMail) { + if (!playerMail || playerMail->receiverId != player->GetObjectID()) { response.status = eAttachmentCollectResponse::MailNotFound; } else if (!inv->HasSpaceForLoot({ {playerMail->itemLOT, playerMail->itemCount} })) { response.status = eAttachmentCollectResponse::NoSpaceInInventory; @@ -225,15 +230,21 @@ namespace Mail { DeleteResponse response; response.mailID = mailID; - auto mailData = Database::Get()->GetMail(mailID); - if (mailData && !(mailData->itemLOT > 0 && mailData->itemCount > 0)) { - Database::Get()->DeleteMail(mailID); - response.status = eDeleteResponse::Success; - } else if (mailData && mailData->itemLOT > 0 && mailData->itemCount > 0) { - response.status = eDeleteResponse::HasAttachments; - } else { - response.status = eDeleteResponse::NotFound; + const auto mailData = Database::Get()->GetMail(mailID); + response.status = eDeleteResponse::NotFound; + if (mailData) { + if (mailData->receiverId != playerID) { + LOG("Player %llu attempted to delete mail owned by %llu. Possible spoof?", playerID, mailData->receiverId); + } else { + if (!(mailData->itemLOT > 0 && mailData->itemCount > 0)) { + Database::Get()->DeleteMail(mailID); + response.status = eDeleteResponse::Success; + } else if (mailData->itemLOT > 0 && mailData->itemCount > 0) { + response.status = eDeleteResponse::HasAttachments; + } + } } + LOG("DeleteRequest status %s", StringifiedEnum::ToString(response.status).data()); response.Send(sysAddr); } @@ -253,11 +264,19 @@ namespace Mail { void ReadRequest::Handle() { ReadResponse response; + response.status = eReadResponse::UnknownError; response.mailID = mailID; - if (Database::Get()->GetMail(mailID)) { - response.status = eReadResponse::Success; - Database::Get()->MarkMailRead(mailID); + const auto mail = Database::Get()->GetMail(mailID); + if (mail) { + if (mail->receiverId == player->GetObjectID()) { + response.status = eReadResponse::Success; + Database::Get()->MarkMailRead(mailID); + } else { + LOG("Player %llu tried to mark mail read for player %llu", mail->receiverId, player->GetObjectID()); + } + } else { + LOG("No mail by ID %llu found to mark as read.", mailID); } LOG("ReadRequest %s", StringifiedEnum::ToString(response.status).data()); diff --git a/dGame/dUtilities/Preconditions.cpp b/dGame/dUtilities/Preconditions.cpp index 0c322987..f0cacad0 100644 --- a/dGame/dUtilities/Preconditions.cpp +++ b/dGame/dUtilities/Preconditions.cpp @@ -114,11 +114,13 @@ bool Precondition::Check(Entity* player, bool evaluateCosts) const { bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluateCosts) const { - auto* missionComponent = player->GetComponent<MissionComponent>(); - auto* inventoryComponent = player->GetComponent<InventoryComponent>(); - auto* destroyableComponent = player->GetComponent<DestroyableComponent>(); - auto* levelComponent = player->GetComponent<LevelProgressionComponent>(); auto* character = player->GetCharacter(); + auto [missionComponent, inventoryComponent, destroyableComponent, levelComponent] = + player->GetComponentsMut<const MissionComponent, /* not const */ InventoryComponent, const DestroyableComponent, const LevelProgressionComponent>(); + + if (!missionComponent || !inventoryComponent || !destroyableComponent || !levelComponent || !character) { + return false; + } Mission* mission; diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index ad4e0463..f1b92e26 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -825,7 +825,7 @@ namespace DEVGMCommands { } const auto numberToSpawnOptional = GeneralUtils::TryParse<uint32_t>(splitArgs[1]); - if (!numberToSpawnOptional && numberToSpawnOptional.value() > 0) { + if (!numberToSpawnOptional) { ChatPackets::SendSystemMessage(sysAddr, u"Invalid number of enemies to spawn."); return; } @@ -833,7 +833,7 @@ namespace DEVGMCommands { // Must spawn within a radius of at least 0.0f const auto radiusToSpawnWithinOptional = GeneralUtils::TryParse<float>(splitArgs[2]); - if (!radiusToSpawnWithinOptional && radiusToSpawnWithinOptional.value() < 0.0f) { + if (!radiusToSpawnWithinOptional || radiusToSpawnWithinOptional.value() < 0.0f) { ChatPackets::SendSystemMessage(sysAddr, u"Invalid radius to spawn within."); return; } @@ -1133,6 +1133,10 @@ namespace DEVGMCommands { } const auto& password = splitArgs[2]; + if (password.length() >= 50) { + ChatPackets::SendSystemMessage(sysAddr, u"Password is too long."); + return; + } ZoneInstanceManager::Instance()->CreatePrivateZone(Game::server, zone.value(), clone.value(), password); diff --git a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp index 9e0bb475..4ef1eb4e 100644 --- a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp @@ -187,8 +187,13 @@ namespace GMZeroCommands { auto splitArgs = GeneralUtils::SplitString(args, ' '); if (splitArgs.empty()) return; - ChatPackets::SendSystemMessage(sysAddr, u"Requesting private map..."); const auto& password = splitArgs[0]; + if (password.length() >= 50) { + ChatPackets::SendSystemMessage(sysAddr, u"Password is too long."); + return; + } + + ChatPackets::SendSystemMessage(sysAddr, u"Requesting private map..."); ZoneInstanceManager::Instance()->RequestPrivateZone(Game::server, false, password, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index aba78800..0ae71f07 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -328,20 +328,14 @@ int main(int argc, char** argv) { } Game::randomEngine = std::mt19937(time(0)); - uint32_t maxClients = 999; - uint32_t ourPort = 2000; - std::string ourIP = "localhost"; - const auto maxClientsString = Game::config->GetValue("max_clients"); - if (!maxClientsString.empty()) maxClients = std::stoi(maxClientsString); - const auto masterServerPortString = Game::config->GetValue("master_server_port"); - if (!masterServerPortString.empty()) ourPort = std::atoi(masterServerPortString.c_str()); - const auto externalIPString = Game::config->GetValue("external_ip"); - if (!externalIPString.empty()) ourIP = externalIPString; + uint32_t maxClients = Game::config->GetValue("max_clients", 999); + uint32_t ourPort = Game::config->GetValue("master_server_port", 2000); + std::string ourIP = Game::config->GetValue("external_ip", "localhost"); char salt[BCRYPT_HASHSIZE]; char hash[BCRYPT_HASHSIZE]; - const auto& cfgPassword = Game::config->GetValue("master_password"); - int res = GenerateBCryptPassword(!cfgPassword.empty() ? cfgPassword : "3.25DARKFLAME1", 13, salt, hash); + const auto& cfgPassword = Game::config->GetValue<std::string>("master_password", "3.25DARKFLAME1"); + int res = GenerateBCryptPassword(cfgPassword, 13, salt, hash); assert(res == 0); Game::server = new dServer(ourIP, ourPort, 0, maxClients, true, false, Game::logger, "", 0, ServiceType::MASTER, Game::config, &Game::lastSignal, hash); @@ -535,8 +529,7 @@ void HandlePacket(Packet* packet) { case MessageType::Master::REQUEST_ZONE_TRANSFER: { LOG("Received zone transfer req"); - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint64_t requestID = 0; uint8_t mythranShift = false; uint32_t zoneID = 0; @@ -575,8 +568,7 @@ void HandlePacket(Packet* packet) { //This is here because otherwise we'd have to include IM in //non-master servers. This packet allows us to add World //servers back if master crashed - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint32_t theirPort = 0; uint32_t theirZoneID = 0; @@ -602,13 +594,13 @@ void HandlePacket(Packet* packet) { instance->SetSysAddr(packet->systemAddress); } } - break; + break; case ServiceType::CHAT: chatServerMasterPeerSysAddr = packet->systemAddress; break; - case ServiceType::AUTH: - authServerMasterPeerSysAddr = packet->systemAddress; - break; + case ServiceType::AUTH: + authServerMasterPeerSysAddr = packet->systemAddress; + break; default: // We just ignore any other server type break; @@ -664,8 +656,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::PLAYER_ADDED: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOMAPID theirZoneID = 0; LWOINSTANCEID theirInstanceID = 0; @@ -684,8 +675,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::PLAYER_REMOVED: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOMAPID theirZoneID = 0; LWOINSTANCEID theirInstanceID = 0; @@ -702,8 +692,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::CREATE_PRIVATE_ZONE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint32_t mapId; LWOCLONEID cloneId; @@ -714,6 +703,8 @@ void HandlePacket(Packet* packet) { uint32_t len; inStream.Read<uint32_t>(len); + len = std::min<uint32_t>(len, 50); // cap the master password at 50 characters + for (uint32_t i = 0; len > i; i++) { char character; inStream.Read<char>(character); @@ -726,8 +717,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::REQUEST_PRIVATE_ZONE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint64_t requestID = 0; uint8_t mythranShift = false; @@ -739,6 +729,7 @@ void HandlePacket(Packet* packet) { uint32_t len; inStream.Read<uint32_t>(len); + len = std::min<uint32_t>(len, 50); for (uint32_t i = 0; i < len; i++) { char character; inStream.Read<char>(character); @@ -761,8 +752,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::WORLD_READY: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOMAPID zoneID; LWOINSTANCEID instanceID; @@ -785,8 +775,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::PREP_ZONE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; int32_t zoneID; inStream.Read(zoneID); @@ -801,8 +790,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::AFFIRM_TRANSFER_RESPONSE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint64_t requestID; @@ -876,7 +864,7 @@ int ShutdownSequence(int32_t signal) { if (!instance->GetIsReady()) { Game::im->RemoveInstance(instance); } - + instance->SetIsShuttingDown(true); } diff --git a/dNavigation/dNavMesh.cpp b/dNavigation/dNavMesh.cpp index d9584b00..a65ed0a3 100644 --- a/dNavigation/dNavMesh.cpp +++ b/dNavigation/dNavMesh.cpp @@ -37,6 +37,10 @@ dNavMesh::~dNavMesh() { void dNavMesh::LoadNavmesh() { + struct FPClose { + FILE* fp{}; + ~FPClose() { if (fp) fclose(fp); } + } fp; std::string path = (BinaryPathFinder::GetBinaryDir() / "navmeshes/" / (std::to_string(m_ZoneId) + ".bin")).string(); @@ -44,55 +48,48 @@ void dNavMesh::LoadNavmesh() { return; } - FILE* fp; - #ifdef _WIN32 - fopen_s(&fp, path.c_str(), "rb"); + fopen_s(&fp.fp, path.c_str(), "rb"); #elif __APPLE__ // macOS has 64bit file IO by default - fp = fopen(path.c_str(), "rb"); + fp.fp = fopen(path.c_str(), "rb"); #else - fp = fopen64(path.c_str(), "rb"); + fp.fp = fopen64(path.c_str(), "rb"); #endif - if (!fp) { + if (!fp.fp) { return; } // Read header. NavMeshSetHeader header; - size_t readLen = fread(&header, sizeof(NavMeshSetHeader), 1, fp); + size_t readLen = fread(&header, sizeof(NavMeshSetHeader), 1, fp.fp); if (readLen != 1) { - fclose(fp); return; } if (header.magic != NAVMESHSET_MAGIC) { - fclose(fp); return; } if (header.version != NAVMESHSET_VERSION) { - fclose(fp); return; } dtNavMesh* mesh = dtAllocNavMesh(); if (!mesh) { - fclose(fp); return; } dtStatus status = mesh->init(&header.params); if (dtStatusFailed(status)) { - fclose(fp); return; } // Read tiles. for (int i = 0; i < header.numTiles; ++i) { NavMeshTileHeader tileHeader; - readLen = fread(&tileHeader, sizeof(tileHeader), 1, fp); + readLen = fread(&tileHeader, sizeof(tileHeader), 1, fp.fp); if (readLen != 1) return; if (!tileHeader.tileRef || !tileHeader.dataSize) @@ -101,14 +98,15 @@ void dNavMesh::LoadNavmesh() { unsigned char* data = static_cast<unsigned char*>(dtAlloc(tileHeader.dataSize, DT_ALLOC_PERM)); if (!data) break; memset(data, 0, tileHeader.dataSize); - readLen = fread(data, tileHeader.dataSize, 1, fp); - if (readLen != 1) return; + readLen = fread(data, tileHeader.dataSize, 1, fp.fp); + if (readLen != 1) { + dtFree(data); + return; + } mesh->addTile(data, tileHeader.dataSize, DT_TILE_FREE_DATA, tileHeader.tileRef, 0); } - fclose(fp); - m_NavMesh = mesh; } @@ -187,13 +185,18 @@ std::vector<NiPoint3> dNavMesh::GetPath(const NiPoint3& startPos, const NiPoint3 path.push_back(startPos); //insert the start pos // Linearly interpolate between these two points: - for (int i = 0; i < numPoints; i++) { - NiPoint3 newPoint{ startPos }; + const auto yDist = endPos.y - startPos.y; + // Ensure we cannot divide by zero + const auto xDist = endPos.x - startPos.x; + if (xDist != 0.0f) { + for (int i = 0; i < numPoints; i++) { + NiPoint3 newPoint{ startPos }; - newPoint.x += speed; - newPoint.y = newPoint.y + (((endPos.y - startPos.y) / (endPos.x - startPos.x)) * (newPoint.x - startPos.x)); + newPoint.x += speed; + newPoint.y = newPoint.y + ((yDist / xDist) * (newPoint.x - startPos.x)); - path.push_back(newPoint); + path.push_back(newPoint); + } } path.push_back(endPos); //finally insert our end pos @@ -212,16 +215,16 @@ std::vector<NiPoint3> dNavMesh::GetPath(const NiPoint3& startPos, const NiPoint3 ePos[2] = endPos.z; dtStatus pathFindStatus; - dtPolyRef startRef; - dtPolyRef endRef; + dtPolyRef startRef{}; + dtPolyRef endRef{}; float polyPickExt[3] = { 32.0f, 32.0f, 32.0f }; dtQueryFilter filter{}; //Find our start poly - m_NavQuery->findNearestPoly(sPos, polyPickExt, &filter, &startRef, 0); + const auto startResult = m_NavQuery->findNearestPoly(sPos, polyPickExt, &filter, &startRef, 0); //Find our end poly - m_NavQuery->findNearestPoly(ePos, polyPickExt, &filter, &endRef, 0); + const auto endResult = m_NavQuery->findNearestPoly(ePos, polyPickExt, &filter, &endRef, 0); pathFindStatus = DT_FAILURE; int m_nstraightPath = 0; @@ -232,7 +235,7 @@ std::vector<NiPoint3> dNavMesh::GetPath(const NiPoint3& startPos, const NiPoint3 dtPolyRef m_straightPathPolys[MAX_POLYS]; int m_straightPathOptions = 0; - if (startRef && endRef) { + if (dtStatusSucceed(startResult) && dtStatusSucceed(endResult)) { m_NavQuery->findPath(startRef, endRef, sPos, ePos, &filter, m_polys, &m_npolys, MAX_POLYS); if (m_npolys) { diff --git a/dNavigation/dTerrain/RawChunk.cpp b/dNavigation/dTerrain/RawChunk.cpp index fe7ac8a2..df4950d4 100644 --- a/dNavigation/dTerrain/RawChunk.cpp +++ b/dNavigation/dTerrain/RawChunk.cpp @@ -18,6 +18,7 @@ RawChunk::RawChunk(std::ifstream& stream) { // We can just skip the rest of the data so we can read the next chunks, we don't need anymore data + // Possible overflow here? TODO make reasonable upper bound or confirm big numbers arent necessary to have uint32_t colorMapSize; BinaryIO::BinaryRead(stream, colorMapSize); stream.seekg(static_cast<uint32_t>(stream.tellg()) + (colorMapSize * colorMapSize * 4)); diff --git a/dNet/AuthPackets.cpp b/dNet/AuthPackets.cpp index 0473ddf2..e1f500b3 100644 --- a/dNet/AuthPackets.cpp +++ b/dNet/AuthPackets.cpp @@ -157,8 +157,7 @@ void AuthPackets::HandleLoginRequest(dServer* server, Packet* packet) { } //If we aren't running in live mode, then only GMs are allowed to enter: - const auto& closedToNonDevs = Game::config->GetValue("closed_to_non_devs"); - if (closedToNonDevs.size() > 0 && bool(std::stoi(closedToNonDevs)) && accountInfo->maxGmLevel == eGameMasterLevel::CIVILIAN) { + if (Game::config->GetValue<bool>("closed_to_non_devs", false) && accountInfo->maxGmLevel == eGameMasterLevel::CIVILIAN) { stamps.emplace_back(eStamps::GM_REQUIRED, 1); AuthPackets::SendLoginResponse(server, packet->systemAddress, eLoginResponse::PERMISSIONS_NOT_HIGH_ENOUGH, "The server is currently only open to developers.", "", 2001, username, stamps); return; diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index 41bf1a2d..43d36e0f 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -108,6 +108,7 @@ ChatModerationRequest ClientPackets::HandleChatModerationRequest(Packet* packet) uint16_t messageLength; inStream.Read(messageLength); + if (messageLength > MAX_MESSAGE_LENGTH) return request; for (uint32_t i = 0; i < messageLength; ++i) { uint16_t character; inStream.Read(character); diff --git a/dNet/MasterPackets.cpp b/dNet/MasterPackets.cpp index aac49929..eb473422 100644 --- a/dNet/MasterPackets.cpp +++ b/dNet/MasterPackets.cpp @@ -77,7 +77,8 @@ void MasterPackets::SendZoneTransferResponse(dServer* server, const SystemAddres void MasterPackets::HandleServerInfo(Packet* packet) { RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + uint64_t header{}; + inStream.Read(header); uint32_t theirPort = 0; uint32_t theirZoneID = 0; diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index 1c9898a9..91117056 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -103,12 +103,9 @@ void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t rep //Compress the data before sending: const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed()); - uint8_t* compressedData = new uint8_t[reservedSize]; + auto compressedData = std::make_unique<uint8_t[]>(reservedSize); - // TODO There should be better handling here for not enough memory... - if (!compressedData) return; - - size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData, reservedSize); + size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize); assert(size <= reservedSize); @@ -123,11 +120,10 @@ void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t rep * an assertion is done to prevent bad data from being saved or sent. */ #pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'. - bitStream.WriteAlignedBytes(compressedData, size); + bitStream.WriteAlignedBytes(compressedData.get(), size); #pragma warning(default:6385) SEND_PACKET; - delete[] compressedData; LOG("Sent CreateCharacter for ID: %llu", player); } diff --git a/dNet/dServer.cpp b/dNet/dServer.cpp index 53009d90..92cfab5b 100644 --- a/dNet/dServer.cpp +++ b/dNet/dServer.cpp @@ -233,13 +233,11 @@ bool dServer::Startup() { } void dServer::UpdateMaximumMtuSize() { - auto maxMtuSize = mConfig->GetValue("maximum_mtu_size"); - mPeer->SetMTUSize(maxMtuSize.empty() ? 1228 : std::stoi(maxMtuSize)); + mPeer->SetMTUSize(mConfig->GetValue<int32_t>("maximum_mtu_size", 1228)); } void dServer::UpdateBandwidthLimit() { - auto newBandwidth = mConfig->GetValue("maximum_outgoing_bandwidth"); - mPeer->SetPerConnectionOutgoingBandwidthLimit(!newBandwidth.empty() ? std::stoi(newBandwidth) : 0); + mPeer->SetPerConnectionOutgoingBandwidthLimit(mConfig->GetValue<int32_t>("maximum_outgoing_bandwidth", 0)); } void dServer::Shutdown() { diff --git a/dPhysics/dpEntity.cpp b/dPhysics/dpEntity.cpp index 6fc40452..6368764f 100644 --- a/dPhysics/dpEntity.cpp +++ b/dPhysics/dpEntity.cpp @@ -109,6 +109,7 @@ void dpEntity::SetPosition(const NiPoint3& newPos) { } void dpEntity::SetRotation(const NiQuaternion& newRot) { + if (!m_CollisionShape) return; m_Rotation = newRot; if (m_CollisionShape->GetShapeType() == dpShapeType::Box) { @@ -118,6 +119,7 @@ void dpEntity::SetRotation(const NiQuaternion& newRot) { } void dpEntity::SetScale(float newScale) { + if (!m_CollisionShape) return; m_Scale = newScale; if (m_CollisionShape->GetShapeType() == dpShapeType::Box) { diff --git a/dPhysics/dpWorld.cpp b/dPhysics/dpWorld.cpp index c9bc742a..a3df390c 100644 --- a/dPhysics/dpWorld.cpp +++ b/dPhysics/dpWorld.cpp @@ -87,7 +87,7 @@ void dpWorld::Shutdown() { } bool dpWorld::IsLoaded() { - return m_NavMesh->IsNavmeshLoaded(); + return m_NavMesh && m_NavMesh->IsNavmeshLoaded(); } void dpWorld::StepWorld(float deltaTime) { diff --git a/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp b/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp index 576688e7..df5a300f 100644 --- a/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp +++ b/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp @@ -36,13 +36,14 @@ void AmSkullkinDrill::OnStartup(Entity* self) { Entity* AmSkullkinDrill::GetStandObj(Entity* self) { const auto& myGroup = self->GetGroups(); - if (myGroup.empty()) { + if (myGroup.empty() || myGroup[0].empty()) { return nullptr; } + const auto& group = myGroup[0]; std::string groupName = "Drill_Stand_"; - groupName.push_back(myGroup[0][myGroup[0].size() - 1]); + groupName.push_back(group.back()); const auto standObjs = Game::entityManager->GetEntitiesInGroup(groupName); diff --git a/dScripts/02_server/Map/General/Binoculars.cpp b/dScripts/02_server/Map/General/Binoculars.cpp index 854ccf71..786c0faf 100644 --- a/dScripts/02_server/Map/General/Binoculars.cpp +++ b/dScripts/02_server/Map/General/Binoculars.cpp @@ -7,9 +7,14 @@ void Binoculars::OnUse(Entity* self, Entity* user) { const auto number = self->GetVarAsString(u"number"); - int32_t flag = std::stoi(std::to_string(Game::server->GetZoneID()).substr(0, 2) + number); - if (user->GetCharacter()->GetPlayerFlag(flag) == false) { - user->GetCharacter()->SetPlayerFlag(flag, true); + const int32_t flag = GeneralUtils::TryParse(std::to_string(Game::server->GetZoneID()).substr(0, 2) + number, 0); + GameMessages::GetFlag flagMsg; + flagMsg.target = user->GetObjectID(); + flagMsg.flagID = flag; + flagMsg.Send(); + if (!flagMsg.flag) { + auto* const character = user->GetCharacter(); + if (character) character->SetPlayerFlag(flag, true); GameMessages::SendFireEventClientSide(self->GetObjectID(), user->GetSystemAddress(), u"achieve", LWOOBJID_EMPTY, 0, -1, LWOOBJID_EMPTY); } } diff --git a/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp b/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp index 4ad78d6a..a5ce92cd 100644 --- a/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp +++ b/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp @@ -33,14 +33,18 @@ void StoryBoxInteractServer::OnUse(Entity* self, Entity* user) { const auto storyText = self->GetVarAsString(u"storyText"); if (storyText.length() > 2) { auto storyValue = GeneralUtils::TryParse<uint32_t>(storyText.substr(storyText.length() - 2)); - if(!storyValue) return; + if (!storyValue) return; int32_t boxFlag = self->GetVar<int32_t>(u"altFlagID"); if (boxFlag <= 0) { boxFlag = (10000 + Game::server->GetZoneID() + storyValue.value()); } - - if (user->GetCharacter()->GetPlayerFlag(boxFlag) == false) { - user->GetCharacter()->SetPlayerFlag(boxFlag, true); + GameMessages::GetFlag flagMsg; + flagMsg.target = user->GetObjectID(); + flagMsg.flagID = boxFlag; + flagMsg.Send(); + if (!flagMsg.flag) { + auto* const character = user->GetCharacter(); + if (character) character->SetPlayerFlag(boxFlag, true); GameMessages::SendFireEventClientSide(self->GetObjectID(), user->GetSystemAddress(), u"achieve", LWOOBJID_EMPTY, 0, -1, LWOOBJID_EMPTY); } } diff --git a/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp b/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp index 554eabac..5306a1dd 100644 --- a/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp +++ b/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp @@ -26,7 +26,8 @@ void NsConcertChoiceBuildManager::SpawnCrate(Entity* self) { const auto splitGroup = GeneralUtils::SplitString(group, '_'); if (splitGroup.size() < 2) return; - const auto groupNumber = std::stoi(splitGroup.at(1)); + const auto groupNumber = GeneralUtils::TryParse(splitGroup.at(1), -1); + if (groupNumber == -1) return; EntityInfo info{}; info.lot = crate.lot; diff --git a/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp b/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp index e64ad2dd..f182c8a5 100644 --- a/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp +++ b/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp @@ -33,7 +33,8 @@ void NtParadoxPanelServer::OnUse(Entity* self, Entity* user) { const auto flag = self->GetVar<int32_t>(u"flag"); - player->GetCharacter()->SetPlayerFlag(flag, true); + auto* const character = player->GetCharacter(); + if (character) character->SetPlayerFlag(flag, true); RenderComponent::PlayAnimation(player, u"rebuild-celebrate"); diff --git a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp index 6f2f6d36..39fae3aa 100644 --- a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp +++ b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp @@ -413,7 +413,8 @@ void ZoneAgProperty::BaseOnFireEventServerSide(Entity* self, Entity* sender, std if (player == nullptr) return; - player->GetCharacter()->SetPlayerFlag(self->GetVar<int32_t>(defeatedProperyFlag), true); + auto* const character = player->GetCharacter(); + if (character) character->SetPlayerFlag(self->GetVar<int32_t>(defeatedProperyFlag), true); GameMessages::SendNotifyClientObject(self->GetObjectID(), u"PlayCinematic", 0, 0, LWOOBJID_EMPTY, destroyedCinematic, UNASSIGNED_SYSTEM_ADDRESS); diff --git a/dScripts/02_server/Map/VE/VeMissionConsole.cpp b/dScripts/02_server/Map/VE/VeMissionConsole.cpp index 011d680d..aa7035eb 100644 --- a/dScripts/02_server/Map/VE/VeMissionConsole.cpp +++ b/dScripts/02_server/Map/VE/VeMissionConsole.cpp @@ -15,7 +15,7 @@ void VeMissionConsole::OnUse(Entity* self, Entity* user) { // The flag to set is 101<number> const auto flagNumber = self->GetVar<std::u16string>(m_NumberVariable); - const int32_t flag = std::stoi("101" + GeneralUtils::UTF16ToWTF8(flagNumber)); + const int32_t flag = GeneralUtils::TryParse("101" + GeneralUtils::UTF16ToWTF8(flagNumber), 0); auto* character = user->GetCharacter(); if (character != nullptr) { diff --git a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp index 8496ecfb..591c29d5 100644 --- a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp +++ b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp @@ -13,7 +13,10 @@ void CavePrisonCage::OnStartup(Entity* self) { return; } - auto* spawner = Game::zoneManager->GetSpawnersByName("PrisonCounterweight_0" + GeneralUtils::UTF16ToWTF8(myNum))[0]; + const auto spawners = Game::zoneManager->GetSpawnersByName("PrisonCounterweight_0" + GeneralUtils::UTF16ToWTF8(myNum)); + if (spawners.empty()) return; + + auto* spawner = spawners[0]; self->SetVar<Spawner*>(u"CWSpawner", spawner); @@ -21,6 +24,7 @@ void CavePrisonCage::OnStartup(Entity* self) { } void CavePrisonCage::Setup(Entity* self, Spawner* spawner) { + if (!spawner) return; SpawnCounterweight(self, spawner); NiPoint3 mypos = self->GetPosition(); @@ -62,6 +66,8 @@ void CavePrisonCage::OnQuickBuildNotifyState(Entity* self, eQuickBuildState stat } void CavePrisonCage::SpawnCounterweight(Entity* self, Spawner* spawner) { + if (!spawner) return; + spawner->Reset(); auto* counterweight = spawner->Spawn(); @@ -164,7 +170,8 @@ void CavePrisonCage::OnTimerDone(Entity* self, std::string timerName) { const auto flagNum = 2020 + self->GetVarAs<int32_t>(u"myNumber"); // Set the flag on the builder character - builder->GetCharacter()->SetPlayerFlag(flagNum, true); + auto* const character = builder->GetCharacter(); + if (character) character->SetPlayerFlag(flagNum, true); // Setup a timer named 'VillagerEscape' to be triggered in 5 seconds self->AddTimer("VillagerEscape", 5.0f); diff --git a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp index 919a2b4f..d7be9d32 100644 --- a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp +++ b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp @@ -408,7 +408,7 @@ void NjMonastryBossInstance::SummonWave(Entity* self, Entity* frakjaw) { // Stop the music for the first, fourth and fifth wave const auto wave = self->GetVar<uint32_t>(WaveNumberVariable); - if (wave >= 1 || wave < (m_Waves.size() - 1)) { + if (wave >= 1 && wave < (m_Waves.size() - 1)) { GameMessages::SendNotifyClientObject(self->GetObjectID(), StopMusicNotification, 0, 0, LWOOBJID_EMPTY, AudioWaveAudio + std::to_string(wave - 1), UNASSIGNED_SYSTEM_ADDRESS); diff --git a/dScripts/BaseConsoleTeleportServer.cpp b/dScripts/BaseConsoleTeleportServer.cpp index d4a49299..d172696c 100644 --- a/dScripts/BaseConsoleTeleportServer.cpp +++ b/dScripts/BaseConsoleTeleportServer.cpp @@ -96,7 +96,7 @@ void BaseConsoleTeleportServer::TransferPlayer(Entity* self, Entity* player, int auto* characterComponent = player->GetComponent<CharacterComponent>(); - if (characterComponent) characterComponent->SendToZone(std::stoi(GeneralUtils::UTF16ToWTF8(teleportZone))); + if (characterComponent) characterComponent->SendToZone(GeneralUtils::TryParse(GeneralUtils::UTF16ToWTF8(teleportZone), 0)); UpdatePlayerTable(self, player, false); } diff --git a/dScripts/BasePropertyServer.cpp b/dScripts/BasePropertyServer.cpp index 9048adeb..69b839f3 100644 --- a/dScripts/BasePropertyServer.cpp +++ b/dScripts/BasePropertyServer.cpp @@ -127,12 +127,15 @@ void BasePropertyServer::BasePlayerLoaded(Entity* self, Entity* player) { if (player->GetObjectID() != propertyOwner) return; } else { - const auto defeatedFlag = player->GetCharacter()->GetPlayerFlag(self->GetVar<int32_t>(defeatedProperyFlag)); + GameMessages::GetFlag flagMsg; + flagMsg.target = player->GetObjectID(); + flagMsg.flagID = self->GetVar<int32_t>(defeatedProperyFlag); + flagMsg.Send(); self->SetNetworkVar(UnclaimedVariable, true); self->SetVar<LWOOBJID>(PlayerIDVariable, player->GetObjectID()); - if (!defeatedFlag) { + if (!flagMsg.flag) { StartMaelstrom(self, player); SpawnSpots(self); GameMessages::SendPlay2DAmbientSound(player, GUIDMaelstrom); diff --git a/dScripts/BaseRandomServer.cpp b/dScripts/BaseRandomServer.cpp index 17817d7a..4b67a5e3 100644 --- a/dScripts/BaseRandomServer.cpp +++ b/dScripts/BaseRandomServer.cpp @@ -124,21 +124,23 @@ void BaseRandomServer::NotifySpawnerOfDeath(Entity* self, Spawner* spawner) { return; } - const auto& sectionName = spawnerName.substr(0, spawnerName.size() - 7); + if (spawnerName.size() >= 7) { + const auto& sectionName = spawnerName.substr(0, spawnerName.size() - 7); - const auto variableName = u"mobsDead" + GeneralUtils::ASCIIToUTF16(sectionName); + const auto variableName = u"mobsDead" + GeneralUtils::ASCIIToUTF16(sectionName); - auto mobDeathCount = self->GetVar<int32_t>(variableName); + auto mobDeathCount = self->GetVar<int32_t>(variableName); - mobDeathCount++; + mobDeathCount++; - if (mobDeathCount >= mobDeathResetNumber) { - const auto& zoneInfo = GeneralUtils::SplitString(sectionName, '_'); + if (mobDeathCount >= mobDeathResetNumber) { + const auto& zoneInfo = GeneralUtils::SplitString(sectionName, '_'); - SpawnSection(self, sectionName, sectionMultipliers[zoneInfo[sectionIDConst - 1]]); + SpawnSection(self, sectionName, sectionMultipliers[zoneInfo[sectionIDConst - 1]]); + } + + self->SetVar(variableName, mobDeathCount); } - - self->SetVar(variableName, mobDeathCount); } void BaseRandomServer::NamedEnemyDeath(Entity* self, Spawner* spawner) { diff --git a/dScripts/BaseRandomServer.h b/dScripts/BaseRandomServer.h index bc5d6b21..daa8bd4c 100644 --- a/dScripts/BaseRandomServer.h +++ b/dScripts/BaseRandomServer.h @@ -37,7 +37,7 @@ public: void NamedTimerDone(Entity* self, const std::string& timerName); protected: - std::vector<int32_t> namedMobs = { + const std::vector<int32_t> namedMobs = { 11988, // Ronin 11984, // Spiderling 12654, // Horsemen diff --git a/dScripts/BaseSurvivalServer.cpp b/dScripts/BaseSurvivalServer.cpp index 07890e71..f71991ba 100644 --- a/dScripts/BaseSurvivalServer.cpp +++ b/dScripts/BaseSurvivalServer.cpp @@ -24,7 +24,8 @@ void BaseSurvivalServer::BasePlayerLoaded(Entity* self, Entity* player) { if (waitingIter != state.waitingPlayers.end() || playersIter != state.players.end()) { auto* characterComponent = player->GetComponent<CharacterComponent>(); - if (characterComponent) characterComponent->SendToZone(player->GetCharacter()->GetLastNonInstanceZoneID()); + const auto* const character = player->GetCharacter(); + if (characterComponent && character) characterComponent->SendToZone(character->GetLastNonInstanceZoneID()); return; } diff --git a/dScripts/NtFactionSpyServer.cpp b/dScripts/NtFactionSpyServer.cpp index 532c128a..7414bb34 100644 --- a/dScripts/NtFactionSpyServer.cpp +++ b/dScripts/NtFactionSpyServer.cpp @@ -73,7 +73,7 @@ void NtFactionSpyServer::OnCinematicUpdate(Entity* self, Entity* sender, eCinema // Make sure we have a path of type <root>_<index> if (pathSplit.size() >= 2) { auto pathRoot = pathSplit.at(0); - auto pathIndex = std::stoi(GeneralUtils::UTF16ToWTF8(pathSplit.at(1))) - 1; + auto pathIndex = GeneralUtils::TryParse(GeneralUtils::UTF16ToWTF8(pathSplit.at(1)), -1) - 1; const auto& dialogueTable = self->GetVar<std::vector<SpyDialogue>>(m_SpyDialogueTableVariable); // Make sure we're listening to the root we're interested in diff --git a/dScripts/SpawnPetBaseServer.cpp b/dScripts/SpawnPetBaseServer.cpp index 395a20c2..d2374162 100644 --- a/dScripts/SpawnPetBaseServer.cpp +++ b/dScripts/SpawnPetBaseServer.cpp @@ -57,7 +57,7 @@ bool SpawnPetBaseServer::CheckNumberOfPets(Entity* self, Entity* user) { if (petID.empty()) continue; - const auto* spawnedPet = Game::entityManager->GetEntity(std::stoull(petID)); + const auto* spawnedPet = Game::entityManager->GetEntity(GeneralUtils::TryParse(petID, LWOOBJID_EMPTY)); if (spawnedPet == nullptr) continue; diff --git a/dScripts/ai/AG/AgFans.cpp b/dScripts/ai/AG/AgFans.cpp index 15f2ed0f..060d29de 100644 --- a/dScripts/ai/AG/AgFans.cpp +++ b/dScripts/ai/AG/AgFans.cpp @@ -24,8 +24,11 @@ void AgFans::OnStartup(Entity* self) { } void AgFans::ToggleFX(Entity* self, bool hit) { - std::string fanGroup = self->GetGroups()[0]; - std::vector<Entity*> fanVolumes = Game::entityManager->GetEntitiesInGroup(fanGroup); + const auto groups = self->GetGroups(); + std::string fanGroup = groups.empty() ? "" : groups[0]; + const auto fanVolumes = Game::entityManager->GetEntitiesInGroup(fanGroup); + const auto fxObjs = Game::entityManager->GetEntitiesInGroup(fanGroup + "fx"); + auto* const fxObj = fxObjs.empty() ? nullptr : fxObjs[0]; auto* renderComponent = static_cast<RenderComponent*>(self->GetComponent(eReplicaComponentType::RENDER)); @@ -47,8 +50,7 @@ void AgFans::ToggleFX(Entity* self, bool hit) { volumePhys->SetPhysicsEffectActive(false); Game::entityManager->SerializeEntity(volume); if (!hit) { - Entity* fxObj = Game::entityManager->GetEntitiesInGroup(fanGroup + "fx")[0]; - RenderComponent::PlayAnimation(fxObj, u"trigger"); + if (fxObj) RenderComponent::PlayAnimation(fxObj, u"trigger"); } } } else if (!self->GetVar<bool>(u"on") && self->GetVar<bool>(u"alive")) { @@ -63,8 +65,7 @@ void AgFans::ToggleFX(Entity* self, bool hit) { volumePhys->SetPhysicsEffectActive(true); Game::entityManager->SerializeEntity(volume); if (!hit) { - Entity* fxObj = Game::entityManager->GetEntitiesInGroup(fanGroup + "fx")[0]; - RenderComponent::PlayAnimation(fxObj, u"idle"); + if (fxObj) RenderComponent::PlayAnimation(fxObj, u"idle"); } } } diff --git a/dScripts/ai/FV/FvBrickPuzzleServer.cpp b/dScripts/ai/FV/FvBrickPuzzleServer.cpp index f8601e3f..77fa0d32 100644 --- a/dScripts/ai/FV/FvBrickPuzzleServer.cpp +++ b/dScripts/ai/FV/FvBrickPuzzleServer.cpp @@ -6,6 +6,7 @@ void FvBrickPuzzleServer::OnStartup(Entity* self) { const auto myGroup = GeneralUtils::UTF16ToWTF8(self->GetVar<std::u16string>(u"spawner_name")); + if (myGroup.size() <= 10) return; const auto pipeNum = GeneralUtils::TryParse<int32_t>(myGroup.substr(10, 1)); if (!pipeNum) return; @@ -17,6 +18,7 @@ void FvBrickPuzzleServer::OnStartup(Entity* self) { void FvBrickPuzzleServer::OnDie(Entity* self, Entity* killer) { const auto myGroup = GeneralUtils::UTF16ToWTF8(self->GetVar<std::u16string>(u"spawner_name")); + if (myGroup.size() <= 10) return; const auto pipeNum = GeneralUtils::TryParse<int32_t>(myGroup.substr(10, 1)); if (!pipeNum) return; diff --git a/dScripts/ai/FV/FvFacilityBrick.cpp b/dScripts/ai/FV/FvFacilityBrick.cpp index 26c07647..1402991c 100644 --- a/dScripts/ai/FV/FvFacilityBrick.cpp +++ b/dScripts/ai/FV/FvFacilityBrick.cpp @@ -9,9 +9,12 @@ void FvFacilityBrick::OnStartup(Entity* self) { } void FvFacilityBrick::OnNotifyObject(Entity* self, Entity* sender, const std::string& name, int32_t param1, int32_t param2) { - auto* brickSpawner = Game::zoneManager->GetSpawnersByName("ImaginationBrick")[0]; - auto* bugSpawner = Game::zoneManager->GetSpawnersByName("MaelstromBug")[0]; - auto* canisterSpawner = Game::zoneManager->GetSpawnersByName("BrickCanister")[0]; + const auto brickObjs = Game::zoneManager->GetSpawnersByName("ImaginationBrick"); + auto* const brickSpawner = brickObjs.empty() ? nullptr : brickObjs[0]; + const auto bugObjs = Game::zoneManager->GetSpawnersByName("MaelstromBug"); + auto* const bugSpawner = bugObjs.empty() ? nullptr : bugObjs[0]; + const auto canisterObjs = Game::zoneManager->GetSpawnersByName("BrickCanister"); + auto* const canisterSpawner = canisterObjs.empty() ? nullptr : canisterObjs[0]; if (name == "ConsoleLeftUp") { GameMessages::SendStopFXEffect(self, true, "LeftPipeOff"); @@ -62,7 +65,7 @@ void FvFacilityBrick::OnNotifyObject(Entity* self, Entity* sender, const std::st canisterSpawner->Reset(); canisterSpawner->Deactivate(); } else if (self->GetVar<bool>(u"ConsoleLEFTActive") || self->GetVar<bool>(u"ConsoleRIGHTActive")) { - brickSpawner->Activate(); + if (brickSpawner) brickSpawner->Activate(); auto* object = Game::entityManager->GetEntitiesInGroup("Brick")[0]; @@ -70,17 +73,25 @@ void FvFacilityBrick::OnNotifyObject(Entity* self, Entity* sender, const std::st GameMessages::SendStopFXEffect(object, true, "bluebrick"); } - bugSpawner->Reset(); - bugSpawner->Deactivate(); + if (bugSpawner) { + bugSpawner->Reset(); + bugSpawner->Deactivate(); + } - canisterSpawner->Reset(); - canisterSpawner->Activate(); + if (canisterSpawner) { + canisterSpawner->Reset(); + canisterSpawner->Activate(); + } } else { - brickSpawner->Reset(); - brickSpawner->Deactivate(); + if (brickSpawner) { + brickSpawner->Reset(); + brickSpawner->Deactivate(); + } - bugSpawner->Reset(); - bugSpawner->Activate(); + if (bugSpawner) { + bugSpawner->Reset(); + bugSpawner->Activate(); + } } } diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp index e97316e5..3dd2773f 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp @@ -241,7 +241,7 @@ void SGCannon::GameOverTimerFunc(Entity* self) { void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { if (self->GetVar<bool>(GameStartedVariable)) { - const auto spawnNumber = static_cast<uint32_t>(std::stoi(name.substr(7))); + const auto spawnNumber = GeneralUtils::TryParse(name.substr(7), 0); const auto& activeSpawns = self->GetVar<std::vector<SGEnemy>>(ActiveSpawnsVariable); if (activeSpawns.size() <= spawnNumber) { LOG_DEBUG("Trying to spawn %i when spawns size is only %i", spawnNumber, activeSpawns.size()); diff --git a/dScripts/ai/NS/NsConcertQuickBuild.cpp b/dScripts/ai/NS/NsConcertQuickBuild.cpp index 7fd3f125..2278eb5c 100644 --- a/dScripts/ai/NS/NsConcertQuickBuild.cpp +++ b/dScripts/ai/NS/NsConcertQuickBuild.cpp @@ -41,7 +41,9 @@ void NsConcertQuickBuild::OnStartup(Entity* self) { return; // Get the manager of the crate of this quick build - const auto groupNumber = std::stoi(splitGroup.at(3)); + const auto groupNumber = GeneralUtils::TryParse(splitGroup.at(3), -1); + if (groupNumber == -1) return; + const auto managerObjects = Game::entityManager->GetEntitiesInGroup("CB_" + std::to_string(groupNumber)); if (managerObjects.empty()) return; diff --git a/dScripts/ai/NS/NsGetFactionMissionServer.cpp b/dScripts/ai/NS/NsGetFactionMissionServer.cpp index 185bd344..d395cfd8 100644 --- a/dScripts/ai/NS/NsGetFactionMissionServer.cpp +++ b/dScripts/ai/NS/NsGetFactionMissionServer.cpp @@ -42,8 +42,11 @@ void NsGetFactionMissionServer::OnRespondToMission(Entity* self, int missionID, } if (flagID != -1) { - player->GetCharacter()->SetPlayerFlag(ePlayerFlag::JOINED_A_FACTION, true); - player->GetCharacter()->SetPlayerFlag(flagID, true); + auto* const character = player->GetCharacter(); + if (character) { + character->SetPlayerFlag(ePlayerFlag::JOINED_A_FACTION, true); + character->SetPlayerFlag(flagID, true); + } } MissionComponent* mis = static_cast<MissionComponent*>(player->GetComponent(eReplicaComponentType::MISSION)); diff --git a/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp b/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp index 4e77cb55..0670408d 100644 --- a/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp +++ b/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp @@ -33,7 +33,8 @@ void AgPropGuard::OnMissionDialogueOK(Entity* self, Entity* target, int missionI ) { //GameMessages::SendNotifyClientObject(Game::entityManager->GetZoneControlEntity()->GetObjectID(), u"GuardChat", target->GetObjectID(), 0, target->GetObjectID(), "", target->GetSystemAddress()); - target->GetCharacter()->SetPlayerFlag(113, true); + auto* const character = target->GetCharacter(); + if (character) character->SetPlayerFlag(113, true); Game::entityManager->GetZoneControlEntity()->AddTimer("GuardFlyAway", 1.0f); } diff --git a/dServer/CMakeLists.txt b/dServer/CMakeLists.txt index ca4e6198..988465df 100644 --- a/dServer/CMakeLists.txt +++ b/dServer/CMakeLists.txt @@ -8,3 +8,5 @@ target_include_directories(dServer PUBLIC ".") target_include_directories(dServer PRIVATE "${PROJECT_SOURCE_DIR}/dCommon/" # BinaryPathFinder.h ) + +target_link_libraries(dServer PRIVATE dCommon) diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 34b485df..48a11cf2 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -763,7 +763,8 @@ void HandleMasterPacket(Packet* packet) { case MessageType::Master::NEW_SESSION_ALERT: { CINSTREAM_SKIP_HEADER; - uint32_t sessionKey = inStream.Read(sessionKey); + uint32_t sessionKey{}; + inStream.Read(sessionKey); LUString username; inStream.Read(username); @@ -970,8 +971,7 @@ void HandlePacket(Packet* packet) { } case MessageType::World::LOGIN_REQUEST: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOOBJID playerID = 0; inStream.Read(playerID); @@ -1256,13 +1256,22 @@ void HandlePacket(Packet* packet) { return; } - Entity* entity = Game::entityManager->GetEntity(user->GetLastUsedChar()->GetObjectID()); - if (entity) entity->ProcessPositionUpdate(positionUpdate); + if (const auto* const lastChar = user->GetLastUsedChar()) { + if (auto* const entity = Game::entityManager->GetEntity(lastChar->GetObjectID())) { + entity->ProcessPositionUpdate(positionUpdate); + } + } break; } case MessageType::World::MAIL: { - Mail::HandleMail(inStream, packet->systemAddress, UserManager::Instance()->GetUser(packet->systemAddress)->GetLastUsedChar()->GetEntity()); + if (auto* const user = UserManager::Instance()->GetUser(packet->systemAddress)) { + if (auto* const lastChar = user->GetLastUsedChar()) { + if (auto* const entity = lastChar->GetEntity()) { + Mail::HandleMail(inStream, packet->systemAddress, entity); + } + } + } break; } @@ -1285,7 +1294,8 @@ void HandlePacket(Packet* packet) { LWOOBJID objectID = 0; auto user = UserManager::Instance()->GetUser(packet->systemAddress); if (user) { - objectID = user->GetLastUsedChar()->GetObjectID(); + const auto* const lastChar = user->GetLastUsedChar(); + if (lastChar) objectID = lastChar->GetObjectID(); } bitStream.Write(objectID); @@ -1385,13 +1395,19 @@ void HandlePacket(Packet* packet) { return; } - if (user->GetIsMuted()) { - user->GetLastUsedChar()->SendMuteNotice(); + const auto* const lastChar = user->GetLastUsedChar(); + if (!lastChar) { + LOG("No last used character for chat message %i", user->GetAccountID()); return; } - std::string playerName = user->GetLastUsedChar()->GetName(); - bool isMythran = user->GetLastUsedChar()->GetGMLevel() > eGameMasterLevel::CIVILIAN; - bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(chatMessage.message), user->GetLastUsedChar()->GetGMLevel()).empty(); + + if (user->GetIsMuted()) { + lastChar->SendMuteNotice(); + return; + } + std::string playerName = lastChar->GetName(); + bool isMythran = lastChar->GetGMLevel() > eGameMasterLevel::CIVILIAN; + bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(chatMessage.message), lastChar->GetGMLevel()).empty(); LOG_DEBUG("Msg: %s was approved previously? %i", GeneralUtils::UTF16ToWTF8(chatMessage.message).c_str(), user->GetLastChatMessageApproved()); if (!isOk) return; if (!isOk && !isMythran) return; diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index 13c85c1a..40ee6d30 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -43,11 +43,11 @@ void Level::MakeSpawner(SceneObject obj) { for (LDFBaseData* data : obj.settings) { if (!data) continue; if (data->GetKey() == u"spawntemplate") { - spawnInfo.templateID = std::stoi(data->GetValueAsString()); + spawnInfo.templateID = GeneralUtils::TryParse(data->GetValueAsString(), 0); } if (data->GetKey() == u"spawner_node_id") { - node->nodeID = std::stoi(data->GetValueAsString()); + node->nodeID = GeneralUtils::TryParse(data->GetValueAsString(), 0u); } if (data->GetKey() == u"spawner_name") { @@ -55,35 +55,34 @@ void Level::MakeSpawner(SceneObject obj) { } if (data->GetKey() == u"max_to_spawn") { - spawnInfo.maxToSpawn = std::stoi(data->GetValueAsString()); + spawnInfo.maxToSpawn = GeneralUtils::TryParse(data->GetValueAsString(), 0); } if (data->GetKey() == u"spawner_active_on_load") { - spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); + spawnInfo.activeOnLoad = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"active_on_load") { - spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); + spawnInfo.activeOnLoad = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"respawn") { if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds { - spawnInfo.respawnTime = std::stof(data->GetValueAsString()); + spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0.0f); } else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms? { - spawnInfo.respawnTime = std::stoul(data->GetValueAsString()) / 1000; + spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0) / 1000; } } if (data->GetKey() == u"spawnsGroupOnSmash") { - spawnInfo.spawnsOnSmash = std::stoi(data->GetValueAsString()); + spawnInfo.spawnsOnSmash = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"spawnNetNameForSpawnGroupOnSmash") { spawnInfo.spawnOnSmashGroupName = data->GetValueAsString(); } if (data->GetKey() == u"groupID") { // Load object groups - std::string groupStr = data->GetValueAsString(); - spawnInfo.groups = GeneralUtils::SplitString(groupStr, ';'); + spawnInfo.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';'); if (spawnInfo.groups.back().empty()) spawnInfo.groups.erase(spawnInfo.groups.end() - 1); } if (data->GetKey() == u"no_auto_spawn") { @@ -236,10 +235,11 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { BinaryIO::BinaryRead(file, obj.lot); if (header.fileInfo.version >= 38) { - uint32_t tmp = 1; + int32_t tmp = 1; BinaryIO::BinaryRead(file, tmp); if (tmp > -1 && tmp < 11) obj.nodeType = tmp; } + if (header.fileInfo.version >= 32) { BinaryIO::BinaryRead(file, obj.glomId); } @@ -290,7 +290,7 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { } // If this is a client only object, we can skip loading it if (data->GetKey() == u"loadOnClientOnly") { - skipLoadingObject |= static_cast<bool>(std::stoi(data->GetValueAsString())); + skipLoadingObject |= GeneralUtils::TryParse(data->GetValueAsString(), false); break; } } diff --git a/dZoneManager/Spawner.cpp b/dZoneManager/Spawner.cpp index 3baf193f..df966055 100644 --- a/dZoneManager/Spawner.cpp +++ b/dZoneManager/Spawner.cpp @@ -53,7 +53,7 @@ Spawner::Spawner(const SpawnerInfo info) { } for (Spawner* ssSpawner : spawnSmashSpawnersN) { m_SpawnSmashFoundGroup = true; - m_SpawnOnSmash = ssSpawner; + m_SpawnOnSmashID = ssSpawner ? ssSpawner->m_Info.spawnerID : LWOOBJID_EMPTY; ssSpawner->AddSpawnedEntityDieCallback([=, this]() { Spawn(); }); @@ -185,12 +185,14 @@ void Spawner::Update(const float deltaTime) { } return; } - for (size_t i = 0; i < m_WaitTimes.size(); ++i) { + for (size_t i = 0; i < m_WaitTimes.size(); ) { m_WaitTimes[i] += deltaTime; if (m_WaitTimes[i] >= m_Info.respawnTime) { m_WaitTimes.erase(m_WaitTimes.begin() + i); Spawn(); + } else { + i++; } } } @@ -222,15 +224,18 @@ void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) { return; } - for (size_t i = 0; i < node->entities.size(); ++i) { + for (size_t i = 0; i < node->entities.size();) { if (node->entities[i] && node->entities[i] == objectID) node->entities.erase(node->entities.begin() + i); + else + i++; } m_Entities.erase(objectID); - if (m_SpawnOnSmash != nullptr) { - m_SpawnOnSmash->Reset(); + auto* const spawnOnSmash = Game::zoneManager->GetSpawner(m_SpawnOnSmashID); + if (spawnOnSmash) { + spawnOnSmash->Reset(); } } diff --git a/dZoneManager/Spawner.h b/dZoneManager/Spawner.h index f96e1892..701ceb29 100644 --- a/dZoneManager/Spawner.h +++ b/dZoneManager/Spawner.h @@ -82,7 +82,7 @@ private: EntityInfo m_EntityInfo; int32_t m_AmountSpawned = 0; bool m_Start = false; - Spawner* m_SpawnOnSmash = nullptr; + LWOOBJID m_SpawnOnSmashID = LWOOBJID_EMPTY; }; #endif // SPAWNER_H diff --git a/resources/chatconfig.ini b/resources/chatconfig.ini index 1391df44..7c6bc28c 100644 --- a/resources/chatconfig.ini +++ b/resources/chatconfig.ini @@ -12,3 +12,5 @@ web_server_enabled=0 # Unused for now # web_server_listen_ip=127.0.0.1 web_server_listen_port=2005 + +max_ignores=32 diff --git a/tests/dCommonTests/AMFDeserializeTests.cpp b/tests/dCommonTests/AMFDeserializeTests.cpp index 0a8c0ac7..759e6fc4 100644 --- a/tests/dCommonTests/AMFDeserializeTests.cpp +++ b/tests/dCommonTests/AMFDeserializeTests.cpp @@ -213,7 +213,7 @@ TEST(dCommonTests, AMFDeserializeUnimplementedValuesTest) { bool caughtException = false; try { ReadFromBitStream(testBitStream); - } catch (eAmf unimplementedValueType) { + } catch (std::exception& e) { caughtException = true; } From 1d2de705fbadd1c11552554e537731e26b3573d0 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:44:29 -0700 Subject: [PATCH 43/69] fix: old man npc mission (#1982) * fix: old man npc mission tested that the repeatable daily now has to actually be done and also can actually be done. * Update OldManNPC.cpp --- dGame/dUtilities/Preconditions.cpp | 1 + dScripts/02_server/Map/njhub/CMakeLists.txt | 1 + dScripts/02_server/Map/njhub/OldManNPC.cpp | 31 +++++++++++++++++++++ dScripts/02_server/Map/njhub/OldManNPC.h | 10 +++++++ dScripts/CppScripts.cpp | 2 ++ 5 files changed, 45 insertions(+) create mode 100644 dScripts/02_server/Map/njhub/OldManNPC.cpp create mode 100644 dScripts/02_server/Map/njhub/OldManNPC.h diff --git a/dGame/dUtilities/Preconditions.cpp b/dGame/dUtilities/Preconditions.cpp index f0cacad0..da18c81c 100644 --- a/dGame/dUtilities/Preconditions.cpp +++ b/dGame/dUtilities/Preconditions.cpp @@ -230,6 +230,7 @@ PreconditionExpression::PreconditionExpression(const std::string& conditions) { case '&': case ';': case '(': + case ':': b << conditions.substr(i + 1); done = true; break; diff --git a/dScripts/02_server/Map/njhub/CMakeLists.txt b/dScripts/02_server/Map/njhub/CMakeLists.txt index 94d99867..367d4647 100644 --- a/dScripts/02_server/Map/njhub/CMakeLists.txt +++ b/dScripts/02_server/Map/njhub/CMakeLists.txt @@ -18,6 +18,7 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_NJHUB "NjJayMissionItems.cpp" "NjNPCMissionSpinjitzuServer.cpp" "NjNyaMissionitems.cpp" + "OldManNPC.cpp" "NjScrollChestServer.cpp" "NjWuNPC.cpp" "RainOfArrows.cpp") diff --git a/dScripts/02_server/Map/njhub/OldManNPC.cpp b/dScripts/02_server/Map/njhub/OldManNPC.cpp new file mode 100644 index 00000000..4d1f1056 --- /dev/null +++ b/dScripts/02_server/Map/njhub/OldManNPC.cpp @@ -0,0 +1,31 @@ +#include "OldManNPC.h" + +#include "eMissionState.h" +#include "Character.h" +#include "MissionComponent.h" + +void ResetMissions(Entity& user) { + for (int32_t i = 1; i < 7; i++) { + int32_t flag = 2020 + i; + auto* const character = user.GetCharacter(); + if (character) character->SetPlayerFlag(flag, false); + } +} + +void OldManNPC::OnUse(Entity* self, Entity* user) { + LOG(""); + const auto* const missionComponent = user->GetComponent<MissionComponent>(); + if (!missionComponent) return; + + const auto* const mission = missionComponent->GetMission(2039); + if (!mission) { + ResetMissions(*user); // shouldnt be needed for dlu but it is because the mission is null + return; + } + + 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/Map/njhub/OldManNPC.h b/dScripts/02_server/Map/njhub/OldManNPC.h new file mode 100644 index 00000000..a7924f98 --- /dev/null +++ b/dScripts/02_server/Map/njhub/OldManNPC.h @@ -0,0 +1,10 @@ +#ifndef OLDMANNPC_H +#define OLDMANNPC_H + +#include "CppScripts.h" + +class OldManNPC : public CppScripts::Script { + void OnUse(Entity* self, Entity* user) override; +}; + +#endif //!OLDMANNPC_H diff --git a/dScripts/CppScripts.cpp b/dScripts/CppScripts.cpp index 4c0ba749..6300716f 100644 --- a/dScripts/CppScripts.cpp +++ b/dScripts/CppScripts.cpp @@ -278,6 +278,7 @@ #include "NjEarthPetServer.h" #include "NjDragonEmblemChestServer.h" #include "NjNyaMissionitems.h" +#include "OldManNPC.h" // Scripted equipment #include "AnvilOfArmor.h" @@ -628,6 +629,7 @@ namespace { {"scripts\\02_server\\Map\\njhub\\L_EARTH_PET_SERVER.lua", []() {return new NjEarthPetServer();}}, {"scripts\\02_server\\Map\\njhub\\L_DRAGON_EMBLEM_CHEST_SERVER.lua", []() {return new NjDragonEmblemChestServer();}}, {"scripts\\02_server\\Map\\njhub\\L_NYA_MISSION_ITEMS.lua", []() {return new NjNyaMissionitems();}}, + {"scripts\\02_server\\Map\\njhub\\L_OLD_MAN_NPC.lua", []() {return new OldManNPC();}}, //DLU {"scripts\\02_server\\DLU\\DLUVanityTeleportingObject.lua", []() {return new DLUVanityTeleportingObject();}}, From ca0da9d3bfc5fad6f39a4e8209fbbaf1e583a108 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell <aronwk.aaron@gmail.com> Date: Mon, 8 Jun 2026 22:45:24 -0500 Subject: [PATCH 44/69] feat: preconditions improvements (#1983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement missing precondition types (20, 21, 23) and pet checks Add DoesNotHaveFlag (23), NotFreeTrial (20), and MissionActive (21) to PreconditionType and implement their checks. Also implement PetDeployed and IsPetTaming using PetComponent static helpers, matching client behavior — both are simple boolean checks with no LOT comparison. LegoClubMember is set to always pass as DLU has no membership concept. * fix: update TODO comments for team check and racing licence preconditions * type Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dGame/dUtilities/Preconditions.cpp | 27 ++++++++++++++++++++------- dGame/dUtilities/Preconditions.h | 5 ++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/dGame/dUtilities/Preconditions.cpp b/dGame/dUtilities/Preconditions.cpp index da18c81c..26f1f7bd 100644 --- a/dGame/dUtilities/Preconditions.cpp +++ b/dGame/dUtilities/Preconditions.cpp @@ -13,6 +13,7 @@ #include "DestroyableComponent.h" #include "GameMessages.h" #include "eMissionState.h" +#include "PetComponent.h" std::map<uint32_t, Precondition*> Preconditions::cache = {}; @@ -79,6 +80,9 @@ bool Precondition::Check(Entity* player, bool evaluateCosts) const { case PreconditionType::DoesNotHaveRacingLicence: case PreconditionType::LegoClubMember: case PreconditionType::NoInteraction: + case PreconditionType::NotFreeTrial: + case PreconditionType::MissionActive: + case PreconditionType::DoesNotHaveFlag: any = true; break; case PreconditionType::DoesNotHaveItem: @@ -154,7 +158,7 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat if (missionComponent == nullptr) return false; return missionComponent->GetMissionState(value) >= eMissionState::COMPLETE; case PreconditionType::PetDeployed: - return false; // TODO + return PetComponent::GetActivePet(player->GetObjectID()) != nullptr; case PreconditionType::HasFlag: return character->GetPlayerFlag(value); case PreconditionType::WithinShape: @@ -162,9 +166,9 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat case PreconditionType::InBuild: return character->GetBuildMode(); case PreconditionType::TeamCheck: - return false; // TODO + return false; // TODO: requires knowing the player's minigame team assignment (red/blue etc.); DLU does not track this per-player case PreconditionType::IsPetTaming: - return false; // TODO + return PetComponent::GetTamingPet(player->GetObjectID()) != nullptr; case PreconditionType::HasFaction: for (const auto faction : destroyableComponent->GetFactionIDs()) { if (faction == static_cast<int>(value)) { @@ -182,15 +186,24 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat return true; case PreconditionType::HasRacingLicence: - return false; // TODO + return false; // TODO: requires a racing licence level on the player; DLU does not track this case PreconditionType::DoesNotHaveRacingLicence: - return false; // TODO + return false; // TODO: requires a racing licence level on the player; DLU does not track this case PreconditionType::LegoClubMember: - return false; // TODO + return true; // Live LU opened LEGO CLUB to All players at some point, so always return true case PreconditionType::NoInteraction: - return false; // TODO + return false; // TODO: requires tracking the player's currently active interaction object; DLU does not track this case PreconditionType::HasLevel: return levelComponent->GetLevel() >= value; + case PreconditionType::NotFreeTrial: + return true; // DLU does not support free trial accounts; all players pass this check + case PreconditionType::MissionActive: { + if (missionComponent == nullptr) return false; + const auto state = missionComponent->GetMissionState(value); + return state == eMissionState::ACTIVE || state == eMissionState::COMPLETE_ACTIVE; + } + case PreconditionType::DoesNotHaveFlag: + return !character->GetPlayerFlag(value); default: return true; // There are a couple more unknown preconditions. Always return true in this case. } diff --git a/dGame/dUtilities/Preconditions.h b/dGame/dUtilities/Preconditions.h index 2b6e1216..0a1ac70b 100644 --- a/dGame/dUtilities/Preconditions.h +++ b/dGame/dUtilities/Preconditions.h @@ -26,7 +26,10 @@ enum class PreconditionType DoesNotHaveRacingLicence, LegoClubMember, NoInteraction, - HasLevel = 22 + NotFreeTrial, + MissionActive, + HasLevel, + DoesNotHaveFlag = 23 }; From 045e097b139bf8746d219edf2d2fb6b0b383f3cd Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:57:33 -0700 Subject: [PATCH 45/69] chore: cleanup zoneIM (#1985) --- dNet/ZoneInstanceManager.cpp | 43 ++++++++++-------------------------- dNet/ZoneInstanceManager.h | 17 +++++--------- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/dNet/ZoneInstanceManager.cpp b/dNet/ZoneInstanceManager.cpp index 354d3634..3be4b9a8 100644 --- a/dNet/ZoneInstanceManager.cpp +++ b/dNet/ZoneInstanceManager.cpp @@ -1,26 +1,17 @@ -#define _VARIADIC_MAX 10 #include "ZoneInstanceManager.h" // Custom Classes #include "MasterPackets.h" -#include "dServer.h" - -// C++ -#include <future> // Static Variables ZoneInstanceManager* ZoneInstanceManager::m_Address = nullptr; //! Requests a zone transfer -void ZoneInstanceManager::RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, std::function<void(bool, uint32_t, uint32_t, uint32_t, std::string, uint16_t)> callback) { +void ZoneInstanceManager::RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, TransferCallback callback) { + const auto nextID = ++currentRequestID; + requests[nextID] = callback; - ZoneTransferRequest* request = new ZoneTransferRequest(); - request->requestID = ++currentRequestID; - request->callback = callback; - - this->requests.push_back(request); - - MasterPackets::SendZoneTransferRequest(server, request->requestID, mythranShift, zoneID, zoneClone); + MasterPackets::SendZoneTransferRequest(server, nextID, mythranShift, zoneID, zoneClone); } //! Handles a zone transfer response @@ -43,18 +34,11 @@ void ZoneInstanceManager::HandleRequestZoneTransferResponse(Packet* packet) { LUString serverIP(255); inStream.Read(serverIP); - for (uint32_t i = 0; i < this->requests.size(); ++i) { - if (this->requests[i]->requestID == requestID) { - - // Call the request callback - this->requests[i]->callback(mythranShift, zoneID, zoneInstance, zoneClone, serverIP.string, serverPort); - - delete this->requests[i]; - this->requests.erase(this->requests.begin() + i); - return; - } + const auto entry = requests.find(requestID); + if (entry != requests.end()) { + entry->second(mythranShift, zoneID, zoneInstance, zoneClone, serverIP.string, serverPort); + requests.erase(entry); } - } void ZoneInstanceManager::CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password) { @@ -65,12 +49,9 @@ void ZoneInstanceManager::RequestPrivateZone( dServer* server, bool mythranShift, const std::string& password, - std::function<void(bool, uint32_t, uint32_t, uint32_t, std::string, uint16_t)> callback) { - ZoneTransferRequest* request = new ZoneTransferRequest(); - request->requestID = ++currentRequestID; - request->callback = callback; + TransferCallback callback) { + const auto nextID = ++currentRequestID; + requests[nextID] = callback; - this->requests.push_back(request); - - MasterPackets::SendZoneRequestPrivate(server, request->requestID, mythranShift, password); + MasterPackets::SendZoneRequestPrivate(server, nextID, mythranShift, password); } diff --git a/dNet/ZoneInstanceManager.h b/dNet/ZoneInstanceManager.h index 47080cde..b47f92e2 100644 --- a/dNet/ZoneInstanceManager.h +++ b/dNet/ZoneInstanceManager.h @@ -1,6 +1,7 @@ #pragma once #include <functional> +#include <map> #include <vector> #include <string> @@ -14,25 +15,20 @@ class dServer; \brief A class for handling zone transfers and zone-related functions */ - //! The zone request -struct ZoneTransferRequest { - uint64_t requestID; - std::function<void(bool, uint32_t, uint32_t, uint32_t, std::string, uint16_t)> callback; -}; - //! The zone manager class ZoneInstanceManager { private: static ZoneInstanceManager* m_Address; //!< The singleton instance - std::vector<ZoneTransferRequest*> requests; //!< The zone transfer requests + using TransferCallback = std::function<void(bool, uint32_t, uint32_t, uint32_t, std::string, uint16_t)>; + std::map<uint64_t, TransferCallback> requests; //!< The zone transfer requests uint64_t currentRequestID; //!< The current request ID public: //! The singleton method static ZoneInstanceManager* Instance() { - if (m_Address == 0) { + if (m_Address == nullptr) { m_Address = new ZoneInstanceManager; m_Address->currentRequestID = 0; } @@ -47,7 +43,7 @@ public: \param mythranShift Whether or not this is a mythran shift \param callback The callback function */ - void RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, std::function<void(bool, uint32_t, uint32_t, uint32_t, std::string, uint16_t)> callback); + void RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, TransferCallback callback); //! Handles a zone transfer response /*! @@ -58,6 +54,5 @@ public: void CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password); - void RequestPrivateZone(dServer* server, bool mythranShift, const std::string& password, std::function<void(bool, uint32_t, uint32_t, uint32_t, std::string, uint16_t)> callback); - + void RequestPrivateZone(dServer* server, bool mythranShift, const std::string& password, TransferCallback callback); }; From a307f0601a84e4361461d3a5baf15b784b093d2f Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:22:04 -0700 Subject: [PATCH 46/69] fix: bugs in private instances causing master crashes (#1986) * fix: bugs in private instances causing master crashes tested that creating a private instance and shutting down the server no longer crashes master * Update InstanceManager.cpp --- dMasterServer/InstanceManager.cpp | 7 ++++++- dMasterServer/InstanceManager.h | 1 + dMasterServer/MasterServer.cpp | 10 ++++------ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index c2ab3333..6ddd32d5 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -332,6 +332,12 @@ int InstanceManager::GetHardCap(LWOMAPID mapID) { return zone ? zone->population_hard_cap : 12; } +void InstanceManager::PruneUnreadyInstances() { + for (int i = static_cast<int>(m_Instances.size()) - 1; i >= 0; i--) { + if (!m_Instances[i]->GetIsReady()) m_Instances.erase(m_Instances.cbegin() + i); + } +} + void Instance::SetShutdownComplete(const bool value) { m_Shutdown = value; } @@ -359,4 +365,3 @@ bool Instance::IsFull(bool isFriendTransfer) const { return true; } - diff --git a/dMasterServer/InstanceManager.h b/dMasterServer/InstanceManager.h index a6ba6d9a..5481efbc 100644 --- a/dMasterServer/InstanceManager.h +++ b/dMasterServer/InstanceManager.h @@ -133,6 +133,7 @@ public: const InstancePtr& CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID cloneID, const std::string& password); const InstancePtr& FindPrivateInstance(const std::string& password); void SetIsShuttingDown(bool value) { this->m_IsShuttingDown = value; }; + void PruneUnreadyInstances(); private: std::string mExternalIP; diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 0ae71f07..4fd89fbe 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -665,7 +665,7 @@ void HandlePacket(Packet* packet) { inStream.Read(theirInstanceID); const auto& instance = - Game::im->FindInstance(theirZoneID, theirInstanceID); + Game::im->FindInstanceWithPrivate(theirZoneID, theirInstanceID); if (instance) { instance->AddPlayer(Player()); } else { @@ -762,7 +762,7 @@ void HandlePacket(Packet* packet) { LOG("Got world ready %i %i", zoneID, instanceID); - const auto& instance = Game::im->FindInstance(zoneID, instanceID); + const auto& instance = Game::im->FindInstanceWithPrivate(zoneID, instanceID); if (instance == nullptr) { LOG("Failed to find zone to ready"); @@ -858,13 +858,11 @@ int ShutdownSequence(int32_t signal) { } // A server might not be finished spinning up yet, remove all of those here. + // prune the unready ones before looping over all of them + Game::im->PruneUnreadyInstances(); for (const auto& instance : Game::im->GetInstances()) { if (!instance) continue; - if (!instance->GetIsReady()) { - Game::im->RemoveInstance(instance); - } - instance->SetIsShuttingDown(true); } From 93076dc36d62a6a34fb310863c18ca2342c17468 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:42:32 -0700 Subject: [PATCH 47/69] chore: simplify metrics (#1987) * chore: simplify metrics rename to hpp and remove unused includes * feedback --- dCommon/Metrics.cpp | 144 +++++------------- dCommon/Metrics.h | 48 ++++++ dCommon/Metrics.hpp | 61 -------- dGame/EntityManager.cpp | 1 - dGame/LeaderboardManager.cpp | 3 +- dGame/dComponents/BaseCombatAIComponent.cpp | 1 - .../SlashCommands/DEVGMCommands.cpp | 18 +-- dWorldServer/WorldServer.cpp | 3 +- 8 files changed, 100 insertions(+), 179 deletions(-) create mode 100644 dCommon/Metrics.h delete mode 100644 dCommon/Metrics.hpp diff --git a/dCommon/Metrics.cpp b/dCommon/Metrics.cpp index 5232cf78..f196bde6 100644 --- a/dCommon/Metrics.cpp +++ b/dCommon/Metrics.cpp @@ -1,105 +1,77 @@ -#include "Metrics.hpp" +#include "Metrics.h" + +#include "StringifiedEnum.h" #include <chrono> -std::unordered_map<MetricVariable, Metric*> Metrics::m_Metrics = {}; -std::vector<MetricVariable> Metrics::m_Variables = { - MetricVariable::GameLoop, - MetricVariable::PacketHandling, - MetricVariable::UpdateEntities, - MetricVariable::UpdateSpawners, - MetricVariable::Physics, - MetricVariable::UpdateReplica, - MetricVariable::Ghosting, - MetricVariable::CPUTime, - MetricVariable::Sleep, - MetricVariable::Frame, -}; +namespace { + std::unordered_map<MetricVariable, Metric> g_Metrics = {}; + std::vector<MetricVariable> g_Variables = { + MetricVariable::GameLoop, + MetricVariable::PacketHandling, + MetricVariable::UpdateEntities, + MetricVariable::UpdateSpawners, + MetricVariable::Physics, + MetricVariable::UpdateReplica, + MetricVariable::Ghosting, + MetricVariable::CPUTime, + MetricVariable::Sleep, + MetricVariable::Frame, + }; +} void Metrics::AddMeasurement(MetricVariable variable, int64_t value) { - const auto& iter = m_Metrics.find(variable); - - Metric* metric; - - if (iter == m_Metrics.end()) { - metric = new Metric(); - - m_Metrics[variable] = metric; - } else { - metric = iter->second; - } + auto& metric = g_Metrics[variable]; AddMeasurement(metric, value); } -void Metrics::AddMeasurement(Metric* metric, int64_t value) { - const auto index = metric->measurementIndex; +void Metrics::AddMeasurement(Metric& metric, int64_t value) { + const auto index = metric.measurementIndex; - metric->measurements[index] = value; + metric.measurements[index] = value; - if (metric->max == -1 || value > metric->max) { - metric->max = value; - } else if (metric->min == -1 || metric->min > value) { - metric->min = value; + if (metric.max == -1 || value > metric.max) { + metric.max = value; + } else if (metric.min == -1 || metric.min > value) { + metric.min = value; } - if (metric->measurementSize < MAX_MEASURMENT_POINTS) { - metric->measurementSize++; + if (metric.measurementSize < MAX_MEASURMENT_POINTS) { + metric.measurementSize++; } - metric->measurementIndex = (index + 1) % MAX_MEASURMENT_POINTS; + metric.measurementIndex = (index + 1) % MAX_MEASURMENT_POINTS; } -const Metric* Metrics::GetMetric(MetricVariable variable) { - const auto& iter = m_Metrics.find(variable); - - if (iter == m_Metrics.end()) { - return nullptr; - } - - Metric* metric = iter->second; +const Metric& Metrics::GetMetric(MetricVariable variable) { + auto& metric = g_Metrics[variable]; int64_t average = 0; - for (size_t i = 0; i < metric->measurementSize; i++) { - average += metric->measurements[i]; + for (size_t i = 0; i < metric.measurementSize; i++) { + average += metric.measurements[i]; } - average /= metric->measurementSize; + average /= metric.measurementSize; - metric->average = average; + metric.average = average; return metric; } void Metrics::StartMeasurement(MetricVariable variable) { - const auto& iter = m_Metrics.find(variable); + auto& metric = g_Metrics[variable]; - Metric* metric; - - if (iter == m_Metrics.end()) { - metric = new Metric(); - - m_Metrics[variable] = metric; - } else { - metric = iter->second; - } - - metric->activeMeasurement = std::chrono::high_resolution_clock::now(); + metric.activeMeasurement = std::chrono::high_resolution_clock::now(); } void Metrics::EndMeasurement(MetricVariable variable) { const auto end = std::chrono::high_resolution_clock::now(); - const auto& iter = m_Metrics.find(variable); + auto& metric = g_Metrics[variable]; - if (iter == m_Metrics.end()) { - return; - } - - Metric* metric = iter->second; - - const auto elapsed = end - metric->activeMeasurement; + const auto elapsed = end - metric.activeMeasurement; const auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count(); @@ -110,44 +82,12 @@ float Metrics::ToMiliseconds(int64_t nanoseconds) { return static_cast<float>(nanoseconds) / 1e6; } -std::string Metrics::MetricVariableToString(MetricVariable variable) { - switch (variable) { - case MetricVariable::GameLoop: - return "GameLoop"; - case MetricVariable::PacketHandling: - return "PacketHandling"; - case MetricVariable::UpdateEntities: - return "UpdateEntities"; - case MetricVariable::UpdateSpawners: - return "UpdateSpawners"; - case MetricVariable::Physics: - return "Physics"; - case MetricVariable::UpdateReplica: - return "UpdateReplica"; - case MetricVariable::Sleep: - return "Sleep"; - case MetricVariable::CPUTime: - return "CPUTime"; - case MetricVariable::Frame: - return "Frame"; - case MetricVariable::Ghosting: - return "Ghosting"; - - default: - return "Invalid"; - } +const std::string_view Metrics::MetricVariableToString(MetricVariable variable) { + return StringifiedEnum::ToString(variable); } const std::vector<MetricVariable>& Metrics::GetAllMetrics() { - return m_Variables; -} - -void Metrics::Clear() { - for (const auto& pair : m_Metrics) { - delete pair.second; - } - - m_Metrics.clear(); + return g_Variables; } /* RSS Memory utilities diff --git a/dCommon/Metrics.h b/dCommon/Metrics.h new file mode 100644 index 00000000..ddf9bf09 --- /dev/null +++ b/dCommon/Metrics.h @@ -0,0 +1,48 @@ +#pragma once + +#include "dCommonVars.h" +#include <vector> +#include <map> +#include <string_view> +#include <unordered_map> +#include <chrono> + +#define MAX_MEASURMENT_POINTS 1024 + +enum class MetricVariable : int32_t { + GameLoop, + PacketHandling, + UpdateEntities, + UpdateSpawners, + Physics, + UpdateReplica, + Ghosting, + CPUTime, + Sleep, + Frame, +}; + +struct Metric { + int64_t measurements[MAX_MEASURMENT_POINTS] = {}; + size_t measurementIndex = 0; + size_t measurementSize = 0; + int64_t max = -1; + int64_t min = -1; + int64_t average = 0; + std::chrono::time_point<std::chrono::high_resolution_clock> activeMeasurement; +}; + +namespace Metrics { + void AddMeasurement(MetricVariable variable, int64_t value); + void AddMeasurement(Metric& metric, int64_t value); + const Metric& GetMetric(MetricVariable variable); + void StartMeasurement(MetricVariable variable); + void EndMeasurement(MetricVariable variable); + float ToMiliseconds(int64_t nanoseconds); + const std::string_view MetricVariableToString(MetricVariable variable); + const std::vector<MetricVariable>& GetAllMetrics(); + + size_t GetPeakRSS(); + size_t GetCurrentRSS(); + size_t GetProcessID(); +}; diff --git a/dCommon/Metrics.hpp b/dCommon/Metrics.hpp deleted file mode 100644 index c03c914f..00000000 --- a/dCommon/Metrics.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include "dCommonVars.h" -#include <vector> -#include <map> -#include <unordered_map> -#include <chrono> - -#define MAX_MEASURMENT_POINTS 1024 - -enum class MetricVariable : int32_t -{ - GameLoop, - PacketHandling, - UpdateEntities, - UpdateSpawners, - Physics, - UpdateReplica, - Ghosting, - CPUTime, - Sleep, - Frame, -}; - -struct Metric -{ - int64_t measurements[MAX_MEASURMENT_POINTS] = {}; - size_t measurementIndex = 0; - size_t measurementSize = 0; - int64_t max = -1; - int64_t min = -1; - int64_t average = 0; - std::chrono::time_point<std::chrono::high_resolution_clock> activeMeasurement; -}; - -class Metrics -{ -public: - ~Metrics(); - - static void AddMeasurement(MetricVariable variable, int64_t value); - static void AddMeasurement(Metric* metric, int64_t value); - static const Metric* GetMetric(MetricVariable variable); - static void StartMeasurement(MetricVariable variable); - static void EndMeasurement(MetricVariable variable); - static float ToMiliseconds(int64_t nanoseconds); - static std::string MetricVariableToString(MetricVariable variable); - static const std::vector<MetricVariable>& GetAllMetrics(); - - static size_t GetPeakRSS(); - static size_t GetCurrentRSS(); - static size_t GetProcessID(); - - static void Clear(); - -private: - Metrics(); - - static std::unordered_map<MetricVariable, Metric*> m_Metrics; - static std::vector<MetricVariable> m_Variables; -}; diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index 92258afc..fee2b66a 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -10,7 +10,6 @@ #include "SkillComponent.h" #include "SwitchComponent.h" #include "UserManager.h" -#include "Metrics.hpp" #include "dZoneManager.h" #include "MissionComponent.h" #include "Game.h" diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp index a6f13c65..b8e40154 100644 --- a/dGame/LeaderboardManager.cpp +++ b/dGame/LeaderboardManager.cpp @@ -18,7 +18,8 @@ #include "DluAssert.h" #include "CDActivitiesTable.h" -#include "Metrics.hpp" + +#include <chrono> namespace LeaderboardManager { std::map<GameID, Leaderboard::Type> leaderboardCache; diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index 5f6399da..b97211b8 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -23,7 +23,6 @@ #include "SkillComponent.h" #include "QuickBuildComponent.h" #include "DestroyableComponent.h" -#include "Metrics.hpp" #include "CDComponentsRegistryTable.h" #include "CDPhysicsComponentTable.h" #include "dNavMesh.h" diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index f1b92e26..f5dbd29f 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -13,7 +13,7 @@ #include "dpShapeSphere.h" #include "dZoneManager.h" #include "EntityInfo.h" -#include "Metrics.hpp" +#include "Metrics.h" #include "PlayerManager.h" #include "SlashCommandHandler.h" #include "UserManager.h" @@ -1288,18 +1288,14 @@ namespace DEVGMCommands { response.Insert("serverInfo", true); auto* info = response.InsertArray("data"); for (const auto variable : Metrics::GetAllMetrics()) { - auto& metricData = info->PushDebug(StringifiedEnum::ToString(variable)); + auto& metricData = info->PushDebug(Metrics::MetricVariableToString(variable)); - auto* metric = Metrics::GetMetric(variable); + const auto& metric = Metrics::GetMetric(variable); - if (metric == nullptr) { - continue; - } - - metricData.PushDebug<AMFStringValue>("Maximum") = std::to_string(Metrics::ToMiliseconds(metric->max)) + "ms"; - metricData.PushDebug<AMFStringValue>("Minimum") = std::to_string(Metrics::ToMiliseconds(metric->min)) + "ms"; - metricData.PushDebug<AMFStringValue>("Average") = std::to_string(Metrics::ToMiliseconds(metric->average)) + "ms"; - metricData.PushDebug<AMFStringValue>("Measurements Count") = std::to_string(metric->measurementSize); + metricData.PushDebug<AMFStringValue>("Maximum") = std::to_string(Metrics::ToMiliseconds(metric.max)) + "ms"; + metricData.PushDebug<AMFStringValue>("Minimum") = std::to_string(Metrics::ToMiliseconds(metric.min)) + "ms"; + metricData.PushDebug<AMFStringValue>("Average") = std::to_string(Metrics::ToMiliseconds(metric.average)) + "ms"; + metricData.PushDebug<AMFStringValue>("Measurements Count") = std::to_string(metric.measurementSize); } auto& processInfo = info->PushDebug("Process Info"); processInfo.PushDebug<AMFStringValue>("Peak RSS") = std::to_string(static_cast<double>(Metrics::GetPeakRSS()) / 1.024e6) + "MB"; diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 48a11cf2..8de1d4e5 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -14,7 +14,7 @@ #include "dConfig.h" #include "dpWorld.h" #include "dZoneManager.h" -#include "Metrics.hpp" +#include "Metrics.h" #include "PerformanceManager.h" #include "Diagnostics.h" #include "BinaryPathFinder.h" @@ -1538,7 +1538,6 @@ void FinalizeShutdown() { LOG("Shutdown complete, zone (%i), instance (%i)", Game::server->GetZoneID(), g_InstanceID); //Delete our objects here: - Metrics::Clear(); dpWorld::Shutdown(); Database::Destroy("WorldServer"); if (Game::chatFilter) delete Game::chatFilter; From bb8f569354d181a254c520d9847c4ed14801490e Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:05:21 -0700 Subject: [PATCH 48/69] chore: Remove pointer usage in trading (#1988) * chore: Remove pointer usage in trading tested that I could still do a trade * Update TradingManager.h * remove returned object --- dGame/TradingManager.cpp | 42 ++++++++++------------------ dGame/TradingManager.h | 23 +++++++-------- dGame/dGameMessages/GameMessages.cpp | 8 +++--- 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/dGame/TradingManager.cpp b/dGame/TradingManager.cpp index c7143354..43d1c98a 100644 --- a/dGame/TradingManager.cpp +++ b/dGame/TradingManager.cpp @@ -10,6 +10,11 @@ #include "CharacterComponent.h" #include "MissionComponent.h" #include "eMissionTaskType.h" +#include <ranges> + +namespace { + std::unique_ptr<Trade> g_EmptyTrade; +} TradingManager* TradingManager::m_Address = nullptr; @@ -233,55 +238,38 @@ void Trade::SendUpdateToOther(LWOOBJID participant) { GameMessages::SendServerTradeUpdate(other->GetObjectID(), coins, items, other->GetSystemAddress()); } -TradingManager::TradingManager() { -} - -TradingManager::~TradingManager() { - for (const auto& pair : trades) { - delete pair.second; - } - - trades.clear(); -} - -Trade* TradingManager::GetTrade(LWOOBJID tradeId) const { +const std::unique_ptr<Trade>& TradingManager::GetTrade(LWOOBJID tradeId) const { const auto& pair = trades.find(tradeId); - if (pair == trades.end()) return nullptr; + if (pair == trades.end()) return g_EmptyTrade; return pair->second; } -Trade* TradingManager::GetPlayerTrade(LWOOBJID playerId) const { - for (const auto& pair : trades) { - if (pair.second->IsParticipant(playerId)) { - return pair.second; +const std::unique_ptr<Trade>& TradingManager::GetPlayerTrade(LWOOBJID playerId) const { + for (const auto& trade : trades | std::views::values) { + if (trade->IsParticipant(playerId)) { + return trade; } } - return nullptr; + return g_EmptyTrade; } void TradingManager::CancelTrade(const LWOOBJID canceller, LWOOBJID tradeId, const bool sendCancelMessage) { - auto* trade = GetTrade(tradeId); + const auto& trade = GetTrade(tradeId); if (trade == nullptr) return; if (sendCancelMessage) trade->Cancel(canceller); - delete trade; - trades.erase(tradeId); } -Trade* TradingManager::NewTrade(LWOOBJID participantA, LWOOBJID participantB) { +void TradingManager::NewTrade(LWOOBJID participantA, LWOOBJID participantB) { const LWOOBJID tradeId = ObjectIDManager::GenerateObjectID(); - auto* trade = new Trade(tradeId, participantA, participantB); - - trades[tradeId] = trade; + trades.insert_or_assign(tradeId, std::make_unique<Trade>(tradeId, participantA, participantB)); LOG("Created new trade between (%llu) <-> (%llu)", participantA, participantB); - - return trade; } diff --git a/dGame/TradingManager.h b/dGame/TradingManager.h index fa55aa9d..d9984a5b 100644 --- a/dGame/TradingManager.h +++ b/dGame/TradingManager.h @@ -2,15 +2,16 @@ #include "Entity.h" -struct TradeItem -{ +#include <map> +#include <memory> + +struct TradeItem { LWOOBJID itemId; LOT itemLot; uint32_t itemCount; }; -class Trade -{ +class Trade { public: explicit Trade(LWOOBJID tradeId, LWOOBJID participantA, LWOOBJID participantB); ~Trade(); @@ -50,8 +51,7 @@ private: }; -class TradingManager -{ +class TradingManager { public: static TradingManager* Instance() { if (!m_Address) { @@ -61,16 +61,13 @@ public: return m_Address; } - explicit TradingManager(); - ~TradingManager(); - - Trade* GetTrade(LWOOBJID tradeId) const; - Trade* GetPlayerTrade(LWOOBJID playerId) const; + const std::unique_ptr<Trade>& GetTrade(LWOOBJID tradeId) const; + const std::unique_ptr<Trade>& GetPlayerTrade(LWOOBJID playerId) const; void CancelTrade(const LWOOBJID canceller, LWOOBJID tradeId, const bool sendCancelMessage = true); - Trade* NewTrade(LWOOBJID participantA, LWOOBJID participantB); + void NewTrade(LWOOBJID participantA, LWOOBJID participantB); private: static TradingManager* m_Address; //For singleton method - std::unordered_map<LWOOBJID, Trade*> trades; + std::unordered_map<LWOOBJID, std::unique_ptr<Trade>> trades; }; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 3af6cb48..5edd47f2 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -3221,7 +3221,7 @@ void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* LOG("Trade request to (%llu)", i64Invitee); - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade != nullptr) { if (!trade->IsParticipant(i64Invitee)) { @@ -3244,7 +3244,7 @@ void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* } void GameMessages::HandleClientTradeCancel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade == nullptr) return; @@ -3258,7 +3258,7 @@ void GameMessages::HandleClientTradeAccept(RakNet::BitStream& inStream, Entity* LOG("Trade accepted from (%llu) -> (%d)", entity->GetObjectID(), bFirst); - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade == nullptr) return; @@ -3324,7 +3324,7 @@ void GameMessages::HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* LOG("Trade item from (%llu) -> (%llu)/(%llu), (%i), (%llu), (%i), (%i)", entity->GetObjectID(), itemId, itemId2, lot, unknown1, unknown2, unknown3); } - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade == nullptr) return; From 90607bdd5c1a27c0379bccf1d6e65dbc74fad000 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:11:52 -0700 Subject: [PATCH 49/69] fix: setting smashable ignoring lnv settings (#1992) tested that the computer build on crux no longer stays around for +12 seconds --- dGame/Entity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index e9e4b4e8..5dde43e5 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -427,7 +427,7 @@ void Entity::Initialize() { comp->SetMaxArmor(destCompData[0].armor); comp->SetDeathBehavior(destCompData[0].death_behavior); - comp->SetIsSmashable(destCompData[0].isSmashable); + comp->SetIsSmashable(comp->GetIsSmashable() || destCompData[0].isSmashable); comp->SetLootMatrixID(destCompData[0].LootMatrixIndex); comp->SetCurrencyIndex(destCompData[0].CurrencyIndex); From 707880b5fc4c6871af8652ad2f552e06822ed903 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:12:06 -0700 Subject: [PATCH 50/69] fix: fv pipe quick build not spawning as it should (#1991) tested that the pipe now spawns a ROCK that you can build. This ROCK you build spawns the PIPE now. new bug: if you start building the ROCK and stop, the pipe will spawn instead of the previous rock. --- dGame/Entity.cpp | 3 +-- dGame/dComponents/QuickBuildComponent.cpp | 30 +++++++++++++++++++++++ dGame/dComponents/QuickBuildComponent.h | 2 ++ dZoneManager/Spawner.cpp | 3 ++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 5dde43e5..d88aa1d5 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -1613,8 +1613,7 @@ void Entity::Kill(Entity* murderer, const eKillType killType) { else Game::entityManager->DestroyEntity(this); } - const auto& grpNameQBShowBricks = GetVar<std::string>(u"grpNameQBShowBricks"); - + const auto& grpNameQBShowBricks = GetVarAsString(u"grpNameQBShowBricks"); if (!grpNameQBShowBricks.empty()) { auto spawners = Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks); diff --git a/dGame/dComponents/QuickBuildComponent.cpp b/dGame/dComponents/QuickBuildComponent.cpp index 0940d19d..52b2ddee 100644 --- a/dGame/dComponents/QuickBuildComponent.cpp +++ b/dGame/dComponents/QuickBuildComponent.cpp @@ -22,6 +22,8 @@ #include "RenderComponent.h" #include "CppScripts.h" +#include "StringifiedEnum.h" +#include "Amf3.h" QuickBuildComponent::QuickBuildComponent(Entity* const entity, const int32_t componentID) : Component{ entity, componentID } { std::u16string checkPreconditions = entity->GetVar<std::u16string>(u"CheckPrecondition"); @@ -42,6 +44,7 @@ QuickBuildComponent::QuickBuildComponent(Entity* const entity, const int32_t com } SpawnActivator(); + RegisterMsg(&QuickBuildComponent::OnGetObjectReportInfo); } QuickBuildComponent::~QuickBuildComponent() { @@ -568,3 +571,30 @@ void QuickBuildComponent::AddQuickBuildCompleteCallback(const std::function<void void QuickBuildComponent::AddQuickBuildStateCallback(const std::function<void(eQuickBuildState state)>& callback) { m_QuickBuildStateCallbacks.push_back(callback); } + +bool QuickBuildComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& quickbuild = reportInfo.info->PushDebug("Quick Build"); + quickbuild.PushDebug<AMFStringValue>("State") = StringifiedEnum::ToString(m_State).data(); + quickbuild.PushDebug<AMFDoubleValue>("Timer") = m_Timer; + quickbuild.PushDebug<AMFDoubleValue>("TimerIncomplete") = m_TimerIncomplete; + quickbuild.PushDebug("ActivatorPosition").PushDebug(m_ActivatorPosition); + quickbuild.PushDebug<AMFStringValue>("ActivatorId") = std::to_string(m_ActivatorId); + quickbuild.PushDebug<AMFBoolValue>("ShowResetEffect") = m_ShowResetEffect; + quickbuild.PushDebug<AMFDoubleValue>("Taken") = m_Taken; + quickbuild.PushDebug<AMFDoubleValue>("ResetTime") = m_ResetTime; + quickbuild.PushDebug<AMFDoubleValue>("CompleteTime") = m_CompleteTime; + quickbuild.PushDebug<AMFIntValue>("TakeImagination") = m_TakeImagination; + quickbuild.PushDebug<AMFBoolValue>("Interruptible") = m_Interruptible; + quickbuild.PushDebug<AMFBoolValue>("SelfActivator") = m_SelfActivator; + auto& modules = quickbuild.PushDebug("CustomModules"); + for (const auto cmodule : m_CustomModules) modules.PushDebug<AMFIntValue>("Module") = cmodule; + quickbuild.PushDebug<AMFIntValue>("ActivityId") = m_ActivityId; + quickbuild.PushDebug<AMFIntValue>("PostImaginationCost") = m_PostImaginationCost; + quickbuild.PushDebug<AMFDoubleValue>("TimeBeforeSmash") = m_TimeBeforeSmash; + quickbuild.PushDebug<AMFDoubleValue>("TimeBeforeDrain") = m_TimeBeforeDrain; + quickbuild.PushDebug<AMFIntValue>("DrainedImagination") = m_DrainedImagination; + quickbuild.PushDebug<AMFBoolValue>("RepositionPlayer") = m_RepositionPlayer; + quickbuild.PushDebug<AMFDoubleValue>("SoftTimer") = m_SoftTimer; + quickbuild.PushDebug<AMFStringValue>("Builder") = std::to_string(m_Builder); + return true; +} diff --git a/dGame/dComponents/QuickBuildComponent.h b/dGame/dComponents/QuickBuildComponent.h index 8f5f1773..4a3b9ef8 100644 --- a/dGame/dComponents/QuickBuildComponent.h +++ b/dGame/dComponents/QuickBuildComponent.h @@ -261,6 +261,8 @@ public: m_StateDirty = true; } private: + + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * Whether or not the quickbuild state has been changed since we last serialized it. */ diff --git a/dZoneManager/Spawner.cpp b/dZoneManager/Spawner.cpp index df966055..cccdfff0 100644 --- a/dZoneManager/Spawner.cpp +++ b/dZoneManager/Spawner.cpp @@ -55,7 +55,8 @@ Spawner::Spawner(const SpawnerInfo info) { m_SpawnSmashFoundGroup = true; m_SpawnOnSmashID = ssSpawner ? ssSpawner->m_Info.spawnerID : LWOOBJID_EMPTY; ssSpawner->AddSpawnedEntityDieCallback([=, this]() { - Spawn(); + // Intentionally left as a non debug log since i have no idea how much stuff this would affect + LOG("WOULD HAVE SPAWNED %i", m_EntityInfo.lot); }); } } From e5b8e5c6b78f8a3f27463092aeef8a56b4c4d752 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:12:31 -0700 Subject: [PATCH 51/69] fix: buggy hitboxes during ag foot race (#1993) * physics fixes * check ptr --- .../dComponents/ProximityMonitorComponent.cpp | 28 +++++++++++ dGame/dComponents/ProximityMonitorComponent.h | 1 + dPhysics/dpGrid.cpp | 50 +++++++++---------- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/dGame/dComponents/ProximityMonitorComponent.cpp b/dGame/dComponents/ProximityMonitorComponent.cpp index fd3f1b5d..14e440d5 100644 --- a/dGame/dComponents/ProximityMonitorComponent.cpp +++ b/dGame/dComponents/ProximityMonitorComponent.cpp @@ -4,6 +4,8 @@ #include "ControllablePhysicsComponent.h" #include "EntityManager.h" #include "SimplePhysicsComponent.h" +#include "Amf3.h" +#include "dpShapeSphere.h" const std::unordered_set<LWOOBJID> ProximityMonitorComponent::m_EmptyObjectSet = {}; @@ -12,6 +14,7 @@ ProximityMonitorComponent::ProximityMonitorComponent(Entity* parent, const int32 SetProximityRadius(radiusSmall, "rocketSmall"); SetProximityRadius(radiusLarge, "rocketLarge"); } + RegisterMsg(&ProximityMonitorComponent::OnGetObjectReportInfo); } ProximityMonitorComponent::~ProximityMonitorComponent() { @@ -60,6 +63,31 @@ bool ProximityMonitorComponent::IsInProximity(const std::string& name, LWOOBJID return collisions.contains(objectID); } +bool ProximityMonitorComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& proxInfo = reportInfo.info->PushDebug("Proximity Monitor"); + for (const auto& [name, entity] : m_ProximitiesData) { + if (!entity) continue; + auto& proxAmf = proxInfo.PushDebug(name); + const auto* const shape = entity->GetShape(); + if (shape && shape->GetShapeType() == dpShapeType::Sphere) { + const auto* const sphere = static_cast<const dpShapeSphere*>(shape); + proxAmf.PushDebug<AMFDoubleValue>("Radius") = sphere->GetRadius(); + } + proxAmf.PushDebug<AMFBoolValue>("Sleeping") = entity->GetSleeping(); + proxAmf.PushDebug<AMFDoubleValue>("Scale") = entity->GetScale(); + proxAmf.PushDebug<AMFBoolValue>("Gargantuan") = entity->GetIsGargantuan(); + proxAmf.PushDebug<AMFBoolValue>("Static") = entity->GetIsStatic(); + proxAmf.PushDebug("Position").PushDebug(entity->GetPosition()); + proxAmf.PushDebug("Rotation").PushDebug(entity->GetRotation()); + auto& collidingAmf = proxAmf.PushDebug("Colliding Objects"); + for (const auto& colliding : entity->GetCurrentlyCollidingObjects()) { + collidingAmf.PushDebug(std::to_string(colliding)); + } + } + + return true; +} + void ProximityMonitorComponent::Update(float deltaTime) { for (const auto& prox : m_ProximitiesData) { if (!prox.second) continue; diff --git a/dGame/dComponents/ProximityMonitorComponent.h b/dGame/dComponents/ProximityMonitorComponent.h index b83c0df0..b4aa1f1a 100644 --- a/dGame/dComponents/ProximityMonitorComponent.h +++ b/dGame/dComponents/ProximityMonitorComponent.h @@ -64,6 +64,7 @@ public: private: + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * All the proximity sensors for this component, indexed by name */ diff --git a/dPhysics/dpGrid.cpp b/dPhysics/dpGrid.cpp index 7a0db1f8..c338efe3 100644 --- a/dPhysics/dpGrid.cpp +++ b/dPhysics/dpGrid.cpp @@ -2,6 +2,7 @@ #include "dpEntity.h" #include <cmath> +#include <ranges> dpGrid::dpGrid(int numCells, int cellSize) { NUM_CELLS = numCells; @@ -122,38 +123,35 @@ void dpGrid::HandleEntity(dpEntity* entity, dpEntity* other) { void dpGrid::HandleCell(int x, int z, float deltaTime) { auto& entities = m_Cells[x][z]; //vector of entities contained within this cell. - for (auto en : entities) { + for (auto* en : entities) { if (!en) continue; if (en->GetIsStatic() || en->GetSleeping()) continue; //Check against all entities that are in the same cell as us - for (auto other : entities) - HandleEntity(en, other); + for (auto other : entities) HandleEntity(en, other); - //To try neighbouring cells as well: (can be disabled if needed) - //we only check 4 of the 8 neighbouring cells, otherwise we'd get duplicates and cpu cycles wasted... - - if (x > 0 && z > 0) { - for (auto other : m_Cells[x - 1][z - 1]) - HandleEntity(en, other); + // All 8 neighbours in one pass. + // staticOnly=false — canonical 4: covers each dynamic-vs-dynamic pair exactly once, + // since the higher-index cell checks back to the lower-index cell. + // staticOnly=true — skipped 4: dynamic entities there are handled when those cells + // process their own en loop; static ones never drive a loop, so + // we handle them here explicitly to avoid missing exits. + struct NeighbourCheck { int dx, dz; bool staticOnly; }; + constexpr NeighbourCheck kNeighbours[8] = { + { -1, -1, false }, { -1, 0, false }, { 0, -1, false }, { -1, 1, false }, + { 1, -1, true }, { 1, 0, true }, { 0, 1, true }, { 1, 1, true }, + }; + for (auto [dx, dz, staticOnly] : kNeighbours) { + const int nx = x + dx; + const int nz = z + dz; + // Ensure the cell we're checking is within the valid range + if (nx < 0 || nx >= NUM_CELLS || nz < 0 || nz >= NUM_CELLS) continue; + for (auto* other : m_Cells[nx][nz]) { + if (!staticOnly || (other && other->GetIsStatic())) + HandleEntity(en, other); + } } - if (x > 0) { - for (auto other : m_Cells[x - 1][z]) - HandleEntity(en, other); - } - - if (z > 0) { - for (auto other : m_Cells[x][z - 1]) - HandleEntity(en, other); - } - - if (x > 0 && z < NUM_CELLS - 1) { - for (auto other : m_Cells[x - 1][z + 1]) - HandleEntity(en, other); - } - - for (auto& [id, entity] : m_GargantuanObjects) - HandleEntity(en, entity); + for (auto* entity : m_GargantuanObjects | std::views::values) HandleEntity(en, entity); } } From 1e9b18fa9d86ab2593cd1700b65503bf18656da9 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:12:43 -0700 Subject: [PATCH 52/69] chore: cleanup usage of pointers in the activity component (#1989) * chore: cleanup usage of pointers in the activity component * feedback --- dGame/dComponents/ActivityComponent.cpp | 313 ++++++++---------- dGame/dComponents/ActivityComponent.h | 62 ++-- .../02_server/Map/AG/NpcAgCourseStarter.cpp | 20 +- dScripts/ActivityManager.cpp | 2 +- 4 files changed, 167 insertions(+), 230 deletions(-) diff --git a/dGame/dComponents/ActivityComponent.cpp b/dGame/dComponents/ActivityComponent.cpp index 2dfd5e95..7fa73f01 100644 --- a/dGame/dComponents/ActivityComponent.cpp +++ b/dGame/dComponents/ActivityComponent.cpp @@ -22,6 +22,7 @@ #include "eMatchUpdate.h" #include "ServiceType.h" #include "MessageType/Chat.h" +#include "ObjectIDManager.h" #include "CDCurrencyTableTable.h" #include "CDActivityRewardsTable.h" @@ -29,6 +30,11 @@ #include "LeaderboardManager.h" #include "CharacterComponent.h" #include "Amf3.h" +#include <ranges> + +namespace { + const ActivityInstance g_EmptyInstance{ nullptr, CDActivities{} }; +} ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Component(parent, componentID) { RegisterMsg(&ActivityComponent::OnGetObjectReportInfo); @@ -71,9 +77,9 @@ void ActivityComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIniti if (m_DirtyActivityInfo) { outBitStream.Write<uint32_t>(m_ActivityPlayers.size()); if (!m_ActivityPlayers.empty()) { - for (const auto& activityPlayer : m_ActivityPlayers) { - outBitStream.Write<LWOOBJID>(activityPlayer->playerID); - for (const auto& activityValue : activityPlayer->values) { + for (const auto& [playerID, values] : m_ActivityPlayers) { + outBitStream.Write<LWOOBJID>(playerID); + for (const auto& activityValue : values) { outBitStream.Write<float_t>(activityValue); } } @@ -111,78 +117,81 @@ void ActivityComponent::PlayerJoin(Entity* player) { if (HasLobby()) { PlayerJoinLobby(player); } else if (!IsPlayedBy(player)) { - auto* instance = NewInstance(); - instance->AddParticipant(player); + NewInstance().AddParticipant(player); } } void ActivityComponent::PlayerJoinLobby(Entity* player) { if (!m_Parent->HasComponent(eReplicaComponentType::QUICK_BUILD)) GameMessages::SendMatchResponse(player, player->GetSystemAddress(), 0); // tell the client they joined a lobby - LobbyPlayer* newLobbyPlayer = new LobbyPlayer(); - newLobbyPlayer->entityID = player->GetObjectID(); - Lobby* playerLobby = nullptr; + LobbyPlayer newLobbyPlayer{}; + newLobbyPlayer.entityID = player->GetObjectID(); + LWOOBJID playerLobbyID = LWOOBJID_EMPTY; auto* character = player->GetCharacter(); if (character != nullptr) character->SetLastNonInstanceZoneID(Game::zoneManager->GetZone()->GetWorldID()); - for (Lobby* lobby : m_Queue) { - if (lobby->players.size() < m_ActivityInfo.maxTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby->players.size() < m_ActivityInfo.maxTeams) { + for (auto& [lobbyID, lobby] : m_Queue) { + if (lobby.players.size() < m_ActivityInfo.maxTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() < m_ActivityInfo.maxTeams) { // If an empty slot in an existing lobby is found - lobby->players.push_back(newLobbyPlayer); - playerLobby = lobby; + lobby.players.push_back(newLobbyPlayer); + playerLobbyID = lobbyID; // Update the joining player on players already in the lobby, and update players already in the lobby on the joining player - std::string matchUpdateJoined = "player=9:" + std::to_string(player->GetObjectID()) + "\nplayerName=0:" + player->GetCharacter()->GetName(); - for (LobbyPlayer* joinedPlayer : lobby->players) { - auto* entity = joinedPlayer->GetEntity(); + LDFData<LWOOBJID> playerLDF("player", player->GetObjectID()); + LDFData<std::string> playerName("playerName", player->GetCharacter()->GetName()); + std::string matchUpdateJoined = playerLDF.GetString() + "\n" + playerName.GetString(); + for (const auto& joinedPlayer : lobby.players) { + auto* const entity = joinedPlayer.GetEntity(); if (entity == nullptr) { continue; } - std::string matchUpdate = "player=9:" + std::to_string(entity->GetObjectID()) + "\nplayerName=0:" + entity->GetCharacter()->GetName(); + LDFData<LWOOBJID> entityLDF("player", entity->GetObjectID()); + LDFData<std::string> entityName("playerName", entity->GetCharacter()->GetName()); + std::string matchUpdate = entityLDF.GetString() + "\n" + entityName.GetString(); GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchUpdate, eMatchUpdate::PLAYER_ADDED); - PlayerReady(entity, joinedPlayer->ready); + PlayerReady(entity, joinedPlayer.ready); GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateJoined, eMatchUpdate::PLAYER_ADDED); } + break; } } - if (!playerLobby) { + if (playerLobbyID == LWOOBJID_EMPTY) { // If all lobbies are full - playerLobby = new Lobby(); - playerLobby->players.push_back(newLobbyPlayer); - playerLobby->timer = m_ActivityInfo.waitTime / 1000; - m_Queue.push_back(playerLobby); + playerLobbyID = ObjectIDManager::GenerateObjectID(); + auto& newLobby = m_Queue[playerLobbyID]; + newLobby.players.push_back(newLobbyPlayer); + newLobby.timer = m_ActivityInfo.waitTime / 1000; } + const auto& lobby = m_Queue[playerLobbyID]; - if (m_ActivityInfo.maxTeamSize != 1 && playerLobby->players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && playerLobby->players.size() >= m_ActivityInfo.minTeams) { + if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) { // Update the joining player on the match timer - std::string matchTimerUpdate = "time=3:" + std::to_string(playerLobby->timer); - GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY); + LDFData<float> matchTimer("time", lobby.timer); + GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimer.GetString(), eMatchUpdate::PHASE_WAIT_READY); } } void ActivityComponent::PlayerLeave(LWOOBJID playerID) { - // Removes the player from a lobby and notifies the others, not applicable for non-lobby instances - for (Lobby* lobby : m_Queue) { - for (int i = 0; i < lobby->players.size(); ++i) { - if (lobby->players[i]->entityID == playerID) { - std::string matchUpdateLeft = "player=9:" + std::to_string(playerID); - for (LobbyPlayer* lobbyPlayer : lobby->players) { - auto* entity = lobbyPlayer->GetEntity(); + for (auto& lobby : m_Queue | std::views::values) { + for (int i = 0; i < lobby.players.size(); i++) { + const auto& player = lobby.players[i]; + if (player.entityID == playerID) { + LDFData<LWOOBJID> matchUpdateLeft("player", playerID); + for (const auto& lobbyPlayer : lobby.players) { + auto* const entity = lobbyPlayer.GetEntity(); if (entity == nullptr) continue; - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft, eMatchUpdate::PLAYER_REMOVED); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft.GetString(), eMatchUpdate::PLAYER_REMOVED); } - delete lobby->players[i]; - lobby->players[i] = nullptr; - lobby->players.erase(lobby->players.begin() + i); + lobby.players.erase(lobby.players.begin() + i); return; } @@ -191,85 +200,79 @@ void ActivityComponent::PlayerLeave(LWOOBJID playerID) { } void ActivityComponent::Update(float deltaTime) { - std::vector<Lobby*> lobbiesToRemove{}; + std::vector<LWOOBJID> lobbiesToRemove{}; // Ticks all the lobbies, not applicable for non-instance activities - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* player : lobby->players) { - auto* entity = player->GetEntity(); + for (auto& [lobbyID, lobby] : m_Queue) { + for (const auto& player : lobby.players) { + const auto* const entity = player.GetEntity(); if (entity == nullptr) { - PlayerLeave(player->entityID); + PlayerLeave(player.entityID); return; } } - if (lobby->players.empty()) { - lobbiesToRemove.push_back(lobby); + if (lobby.players.empty()) { + lobbiesToRemove.push_back(lobbyID); continue; } // Update the match time for all players - if (m_ActivityInfo.maxTeamSize != 1 && lobby->players.size() >= m_ActivityInfo.minTeamSize - || m_ActivityInfo.maxTeamSize == 1 && lobby->players.size() >= m_ActivityInfo.minTeams) { - if (lobby->timer == m_ActivityInfo.waitTime / 1000) { - for (LobbyPlayer* joinedPlayer : lobby->players) { - auto* entity = joinedPlayer->GetEntity(); + if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize + || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) { + if (lobby.timer == m_ActivityInfo.waitTime / 1000) { + for (const auto& joinedPlayer : lobby.players) { + auto* const entity = joinedPlayer.GetEntity(); if (entity == nullptr) continue; - std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY); + LDFData<float> matchTimerUpdate("time", lobby.timer); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_READY); } } - lobby->timer -= deltaTime; + lobby.timer -= deltaTime; } bool lobbyReady = true; - for (LobbyPlayer* player : lobby->players) { - if (player->ready) continue; + for (const auto& player : lobby.players) { + if (player.ready) continue; lobbyReady = false; } // If everyone's ready, jump the timer - if (lobbyReady && lobby->timer > m_ActivityInfo.startDelay / 1000) { - lobby->timer = m_ActivityInfo.startDelay / 1000; + if (lobbyReady && lobby.timer > m_ActivityInfo.startDelay / 1000) { + lobby.timer = m_ActivityInfo.startDelay / 1000; // Update players in lobby on switch to start delay - std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); - for (LobbyPlayer* player : lobby->players) { - auto* entity = player->GetEntity(); + LDFData<float> matchTimerUpdate("time", lobby.timer); + for (const auto& player : lobby.players) { + auto* const entity = player.GetEntity(); if (entity == nullptr) continue; - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_START); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_START); } } // The timer has elapsed, start the instance - if (lobby->timer <= 0.0f) { + if (lobby.timer <= 0.0f) { LOG("Setting up instance."); - ActivityInstance* instance = NewInstance(); - LoadPlayersIntoInstance(instance, lobby->players); - instance->StartZone(); - lobbiesToRemove.push_back(lobby); + auto& instance = NewInstance(); + LoadPlayersIntoInstance(instance, lobby.players); + instance.StartZone(); + lobbiesToRemove.push_back(lobbyID); } } - while (!lobbiesToRemove.empty()) { - RemoveLobby(lobbiesToRemove.front()); - lobbiesToRemove.erase(lobbiesToRemove.begin()); + for (const auto id : lobbiesToRemove) { + RemoveLobby(id); } } -void ActivityComponent::RemoveLobby(Lobby* lobby) { - for (int i = 0; i < m_Queue.size(); ++i) { - if (m_Queue[i] == lobby) { - m_Queue.erase(m_Queue.begin() + i); - return; - } - } +void ActivityComponent::RemoveLobby(const LWOOBJID lobbyID) { + if (m_Queue.contains(lobbyID)) m_Queue.erase(lobbyID); } bool ActivityComponent::HasLobby() const { @@ -278,9 +281,9 @@ bool ActivityComponent::HasLobby() const { } bool ActivityComponent::PlayerIsInQueue(Entity* player) { - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* lobbyPlayer : lobby->players) { - if (player->GetObjectID() == lobbyPlayer->entityID) return true; + for (const auto& lobby : m_Queue | std::views::values) { + for (const auto& lobbyPlayer : lobby.players) { + if (player->GetObjectID() == lobbyPlayer.entityID) return true; } } @@ -288,8 +291,8 @@ bool ActivityComponent::PlayerIsInQueue(Entity* player) { } bool ActivityComponent::IsPlayedBy(Entity* player) const { - for (const auto* instance : this->m_Instances) { - for (const auto* instancePlayer : instance->GetParticipants()) { + for (const auto& instance : m_Instances) { + for (const auto* instancePlayer : instance.GetParticipants()) { if (instancePlayer != nullptr && instancePlayer->GetObjectID() == player->GetObjectID()) return true; } @@ -299,8 +302,8 @@ bool ActivityComponent::IsPlayedBy(Entity* player) const { } bool ActivityComponent::IsPlayedBy(LWOOBJID playerID) const { - for (const auto* instance : this->m_Instances) { - for (const auto* instancePlayer : instance->GetParticipants()) { + for (const auto& instance : m_Instances) { + for (const auto* instancePlayer : instance.GetParticipants()) { if (instancePlayer != nullptr && instancePlayer->GetObjectID() == playerID) return true; } @@ -329,136 +332,95 @@ bool ActivityComponent::TakeCost(Entity* player) const { } void ActivityComponent::PlayerReady(Entity* player, bool bReady) { - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* lobbyPlayer : lobby->players) { - if (lobbyPlayer->entityID == player->GetObjectID()) { + for (auto& lobby : m_Queue | std::views::values) { + for (auto& lobbyPlayer : lobby.players) { + if (lobbyPlayer.entityID == player->GetObjectID()) { - lobbyPlayer->ready = bReady; + lobbyPlayer.ready = bReady; // Update players in lobby on player being ready - std::string matchReadyUpdate = "player=9:" + std::to_string(player->GetObjectID()); + LDFData<LWOOBJID> matchReadyUpdate("player", player->GetObjectID()); eMatchUpdate readyStatus = eMatchUpdate::PLAYER_READY; if (!bReady) readyStatus = eMatchUpdate::PLAYER_NOT_READY; - for (LobbyPlayer* otherPlayer : lobby->players) { - auto* entity = otherPlayer->GetEntity(); + for (const auto& otherPlayer : lobby.players) { + auto* const entity = otherPlayer.GetEntity(); if (entity == nullptr) continue; - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate, readyStatus); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate.GetString(), readyStatus); } } } } } -ActivityInstance* ActivityComponent::NewInstance() { - auto* instance = new ActivityInstance(m_Parent, m_ActivityInfo); - m_Instances.push_back(instance); - return instance; +ActivityInstance& ActivityComponent::NewInstance() { + m_Instances.push_back(ActivityInstance(m_Parent, m_ActivityInfo)); + return m_Instances.back(); } -void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector<LobbyPlayer*>& lobby) const { - for (LobbyPlayer* player : lobby) { - auto* entity = player->GetEntity(); +void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance& instance, const std::vector<LobbyPlayer>& lobby) const { + for (const auto& player : lobby) { + auto* const entity = player.GetEntity(); if (entity == nullptr || !CheckCost(entity)) { continue; } - instance->AddParticipant(entity); + instance.AddParticipant(entity); } } -const std::vector<ActivityInstance*>& ActivityComponent::GetInstances() const { - return m_Instances; -} - -ActivityInstance* ActivityComponent::GetInstance(const LWOOBJID playerID) { - for (const auto* instance : GetInstances()) { - for (const auto* participant : instance->GetParticipants()) { +const ActivityInstance& ActivityComponent::GetInstance(const LWOOBJID playerID) const { + for (const auto& instance : m_Instances) { + for (const auto* participant : instance.GetParticipants()) { if (participant->GetObjectID() == playerID) - return const_cast<ActivityInstance*>(instance); + return instance; } } - return nullptr; + return g_EmptyInstance; } -void ActivityComponent::ClearInstances() { - for (ActivityInstance* instance : m_Instances) { - delete instance; - } - m_Instances.clear(); -} - -ActivityPlayer* ActivityComponent::GetActivityPlayerData(LWOOBJID playerID) { - for (auto* activityData : m_ActivityPlayers) { - if (activityData->playerID == playerID) { - return activityData; - } - } - - return nullptr; +bool ActivityComponent::PlayerHasActivityData(LWOOBJID playerID) const { + return m_ActivityPlayers.contains(playerID); } void ActivityComponent::RemoveActivityPlayerData(LWOOBJID playerID) { - for (size_t i = 0; i < m_ActivityPlayers.size(); i++) { - if (m_ActivityPlayers[i]->playerID == playerID) { - delete m_ActivityPlayers[i]; - m_ActivityPlayers[i] = nullptr; - - m_ActivityPlayers.erase(m_ActivityPlayers.begin() + i); - m_DirtyActivityInfo = true; - Game::entityManager->SerializeEntity(m_Parent); - - return; - } - } -} - -ActivityPlayer* ActivityComponent::AddActivityPlayerData(LWOOBJID playerID) { - auto* data = GetActivityPlayerData(playerID); - if (data != nullptr) - return data; - - m_ActivityPlayers.push_back(new ActivityPlayer{ playerID, {} }); + m_ActivityPlayers.erase(playerID); m_DirtyActivityInfo = true; - Game::entityManager->SerializeEntity(m_Parent); - - return GetActivityPlayerData(playerID); } -float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) { - auto value = -1.0f; +float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) const { + float value = -1.0f; - auto* data = GetActivityPlayerData(playerID); - if (data != nullptr) { - value = data->values[std::min(index, static_cast<uint32_t>(9))]; + const auto& data = m_ActivityPlayers.find(playerID); + if (data != m_ActivityPlayers.cend()) { + value = data->second[std::min(index, static_cast<uint32_t>(9))]; } - + LOG_DEBUG("Player %llu has score %f at index %i", playerID, value, index); return value; } void ActivityComponent::SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value) { - auto* data = AddActivityPlayerData(playerID); - if (data != nullptr) { - data->values[std::min(index, static_cast<uint32_t>(9))] = value; - } + auto& data = m_ActivityPlayers[playerID]; + data[std::min(index, static_cast<uint32_t>(9))] = value; + LOG_DEBUG("%llu index %i has score of %f", playerID, index, value); m_DirtyActivityInfo = true; Game::entityManager->SerializeEntity(m_Parent); } void ActivityComponent::PlayerRemove(LWOOBJID playerID) { - for (auto* instance : GetInstances()) { - auto participants = instance->GetParticipants(); + for (int i = 0; i < m_Instances.size(); i++) { + auto& instance = m_Instances[i]; + auto participants = instance.GetParticipants(); for (const auto* participant : participants) { if (participant != nullptr && participant->GetObjectID() == playerID) { - instance->RemoveParticipant(participant); + instance.RemoveParticipant(participant); RemoveActivityPlayerData(playerID); // If the instance is empty after the delete of the participant, delete the instance too - if (instance->GetParticipants().empty()) { - m_Instances.erase(std::find(m_Instances.begin(), m_Instances.end(), instance)); - delete instance; + if (instance.GetParticipants().empty()) { + m_Instances.erase(m_Instances.begin() + i); } return; } @@ -595,14 +557,13 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& auto& instances = activityInfo.PushDebug("Instances: " + std::to_string(m_Instances.size())); size_t i = 0; for (const auto& activityInstance : m_Instances) { - if (!activityInstance) continue; auto& instance = instances.PushDebug("Instance " + std::to_string(i++)); - instance.PushDebug<AMFIntValue>("Score") = activityInstance->GetScore(); - instance.PushDebug<AMFIntValue>("Next Zone Clone ID") = activityInstance->GetNextZoneCloneID(); + instance.PushDebug<AMFIntValue>("Score") = activityInstance.GetScore(); + instance.PushDebug<AMFIntValue>("Next Zone Clone ID") = activityInstance.GetNextZoneCloneID(); { auto& activityInfo = instance.PushDebug("Activity Info"); - const auto& instanceActInfo = activityInstance->GetActivityInfo(); + const auto& instanceActInfo = activityInstance.GetActivityInfo(); activityInfo.PushDebug<AMFIntValue>("ActivityID") = instanceActInfo.ActivityID; activityInfo.PushDebug<AMFIntValue>("locStatus") = instanceActInfo.locStatus; activityInfo.PushDebug<AMFIntValue>("instanceMapID") = instanceActInfo.instanceMapID; @@ -625,7 +586,7 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& } auto& participants = instance.PushDebug("Participants"); - for (const auto* participant : activityInstance->GetParticipants()) { + for (const auto* participant : activityInstance.GetParticipants()) { if (!participant) continue; auto* character = participant->GetCharacter(); if (!character) continue; @@ -635,38 +596,36 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& auto& queue = activityInfo.PushDebug("Queue"); i = 0; - for (const auto& lobbyQueue : m_Queue) { + for (const auto& lobbyQueue : m_Queue | std::views::values) { auto& lobby = queue.PushDebug("Lobby " + std::to_string(i++)); - lobby.PushDebug<AMFDoubleValue>("Timer") = lobbyQueue->timer; + lobby.PushDebug<AMFDoubleValue>("Timer") = lobbyQueue.timer; auto& players = lobby.PushDebug("Players"); - for (const auto* player : lobbyQueue->players) { - if (!player) continue; - auto* playerEntity = player->GetEntity(); + for (const auto& player : lobbyQueue.players) { + const auto* const playerEntity = player.GetEntity(); if (!playerEntity) continue; auto* character = playerEntity->GetCharacter(); if (!character) continue; - players.PushDebug<AMFStringValue>(std::to_string(playerEntity->GetObjectID()) + ": " + character->GetName()) = player->ready ? "Ready" : "Not Ready"; + players.PushDebug<AMFStringValue>(std::to_string(playerEntity->GetObjectID()) + ": " + character->GetName()) = player.ready ? "Ready" : "Not Ready"; } } auto& activityPlayers = activityInfo.PushDebug("Activity Players"); - for (const auto* activityPlayer : m_ActivityPlayers) { - if (!activityPlayer) continue; - auto* const activityPlayerEntity = Game::entityManager->GetEntity(activityPlayer->playerID); + for (const auto& [playerID, playerScores] : m_ActivityPlayers) { + auto* const activityPlayerEntity = Game::entityManager->GetEntity(playerID); if (!activityPlayerEntity) continue; auto* character = activityPlayerEntity->GetCharacter(); if (!character) continue; - auto& playerData = activityPlayers.PushDebug(std::to_string(activityPlayer->playerID) + " " + character->GetName()); + auto& playerData = activityPlayers.PushDebug(std::to_string(playerID) + " " + character->GetName()); auto& scores = playerData.PushDebug("Scores"); for (size_t i = 0; i < 10; ++i) { - scores.PushDebug<AMFDoubleValue>(std::to_string(i)) = activityPlayer->values[i]; + scores.PushDebug<AMFDoubleValue>(std::to_string(i)) = playerScores[i]; } } - + activityInfo.PushDebug<AMFIntValue>("ActivityID") = m_ActivityID; return true; } diff --git a/dGame/dComponents/ActivityComponent.h b/dGame/dComponents/ActivityComponent.h index 4f423d8e..856ca70f 100644 --- a/dGame/dComponents/ActivityComponent.h +++ b/dGame/dComponents/ActivityComponent.h @@ -8,14 +8,15 @@ #include "eReplicaComponentType.h" #include "CDActivitiesTable.h" +#include <array> namespace GameMessages { class GameMsg; }; - /** - * Represents an instance of an activity, having participants and score - */ +/** + * Represents an instance of an activity, having participants and score + */ class ActivityInstance { public: ActivityInstance(Entity* parent, CDActivities activityInfo) { m_Parent = parent; m_ActivityInfo = activityInfo; }; @@ -104,7 +105,7 @@ struct LobbyPlayer { /** * The ID of the entity that is in the lobby */ - LWOOBJID entityID; + LWOOBJID entityID = LWOOBJID_EMPTY; /** * Whether or not the entity is ready @@ -126,12 +127,12 @@ struct Lobby { /** * The lobby of players */ - std::vector<LobbyPlayer*> players; + std::vector<LobbyPlayer> players; /** * The timer that determines when the activity should start */ - float timer; + float timer{}; }; /** @@ -142,12 +143,12 @@ struct ActivityPlayer { /** * The entity that the score is tracked for */ - LWOOBJID playerID; + LWOOBJID playerID{}; /** * The list of score for this entity */ - float values[10]; + float values[10]{}; }; /** @@ -194,13 +195,13 @@ public: * @param instance the instance to load the players into * @param lobby the players to load into the instance */ - void LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector<LobbyPlayer*>& lobby) const; + void LoadPlayersIntoInstance(ActivityInstance& instance, const std::vector<LobbyPlayer>& lobby) const; /** * Removes a lobby from the activity manager * @param lobby the lobby to remove */ - void RemoveLobby(Lobby* lobby); + void RemoveLobby(const LWOOBJID lobbyID); /** * Marks a player as (un)ready in a lobby @@ -246,7 +247,7 @@ public: */ bool IsPlayedBy(LWOOBJID playerID) const; - /** + /** * Checks if the entity has enough cost to play this activity * @param player the entity to check * @return true if the entity has enough cost to play this activity, false otherwise @@ -271,20 +272,14 @@ public: * Creates a new instance for this activity * @return a new instance for this activity */ - ActivityInstance* NewInstance(); - - /** - * Returns all the currently active instances of this activity - * @return all the currently active instances of this activity - */ - const std::vector<ActivityInstance*>& GetInstances() const; + ActivityInstance& NewInstance(); /** * Returns the instance that some entity is currently playing in * @param playerID the entity to check for * @return if any, the instance that the entity is currently in */ - ActivityInstance* GetInstance(const LWOOBJID playerID); + const ActivityInstance& GetInstance(const LWOOBJID playerID) const; /** * @brief Reloads the config settings for this component @@ -292,23 +287,12 @@ public: */ void ReloadConfig(); - /** - * Removes all the instances - */ - void ClearInstances(); - - /** - * Returns all the score for the players that are currently playing this activity - * @return - */ - std::vector<ActivityPlayer*> GetActivityPlayers() { return m_ActivityPlayers; }; - /** * Returns activity data for a specific entity (e.g. score and such). * @param playerID the entity to get data for * @return the activity data (score) for the passed player in this activity, if it exists */ - ActivityPlayer* GetActivityPlayerData(LWOOBJID playerID); + bool PlayerHasActivityData(LWOOBJID playerID) const; /** * Sets some score value for an entity @@ -324,7 +308,7 @@ public: * @param index the index to get score for * @return activity score for the passed parameters */ - float_t GetActivityValue(LWOOBJID playerID, uint32_t index); + float_t GetActivityValue(LWOOBJID playerID, uint32_t index) const; /** * Removes activity score tracking for some entity @@ -332,13 +316,6 @@ public: */ void RemoveActivityPlayerData(LWOOBJID playerID); - /** - * Adds activity score tracking for some entity - * @param playerID the entity to add the activity score for - * @return the created entry - */ - ActivityPlayer* AddActivityPlayerData(LWOOBJID playerID); - /** * Sets the mapID that this activity points to * @param mapID the map ID to set @@ -346,7 +323,6 @@ public: void SetInstanceMapID(uint32_t mapID) { m_ActivityInfo.instanceMapID = mapID; }; private: - bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); /** * The database information for this activity @@ -356,17 +332,17 @@ private: /** * All the active instances of this activity */ - std::vector<ActivityInstance*> m_Instances; + std::vector<ActivityInstance> m_Instances; /** * The current lobbies for this activity */ - std::vector<Lobby*> m_Queue; + std::map<LWOOBJID, Lobby> m_Queue; /** * All the activity score for the players in this activity */ - std::vector<ActivityPlayer*> m_ActivityPlayers; + std::map<LWOOBJID, std::array<float, 10>> m_ActivityPlayers; /** * The activity id diff --git a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp index fc724fb9..7e8ca7e8 100644 --- a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp +++ b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp @@ -19,7 +19,7 @@ void NpcAgCourseStarter::OnUse(Entity* self, Entity* user) { const auto userId = user->GetObjectID(); const auto& userSysAddr = user->GetSystemAddress(); - if (scriptedActivityComponent->GetActivityPlayerData(userId) != nullptr) { + if (scriptedActivityComponent->PlayerHasActivityData(userId)) { GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", userSysAddr); } else { GameMessages::SendNotifyClientObject(selfId, u"start", 0, 0, LWOOBJID_EMPTY, "", userSysAddr); @@ -45,18 +45,18 @@ void NpcAgCourseStarter::OnMessageBoxResponse(Entity* self, Entity* sender, int3 GameMessages::SendNotifyClientObject(selfId, u"start_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); GameMessages::SendActivityStart(selfId, senderSysAddr); - auto* const data = scriptedActivityComponent->AddActivityPlayerData(senderId); - if (data->values[1] != 0) return; + const auto score = scriptedActivityComponent->GetActivityValue(senderId, 1); + if (score != 0 && score != -1.0f) return; const auto raceStartTime = Game::server->GetUptime() + std::chrono::seconds(4); // Offset for starting timer const auto fRaceStartTime = std::chrono::duration<float, std::ratio<1>>(raceStartTime).count(); - data->values[1] = fRaceStartTime; + scriptedActivityComponent->SetActivityValue(senderId, 1, fRaceStartTime); Game::entityManager->SerializeEntity(self); } else if (identifier == u"FootRaceCancel") { GameMessages::SendNotifyClientObject(selfId, u"stop_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); - if (scriptedActivityComponent->GetActivityPlayerData(senderId) != nullptr) { + if (scriptedActivityComponent->PlayerHasActivityData(senderId)) { GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); } else { GameMessages::SendNotifyClientObject(selfId, u"start", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); @@ -74,8 +74,7 @@ void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std const auto senderId = sender->GetObjectID(); const auto& senderSysAddr = sender->GetSystemAddress(); - auto* const data = scriptedActivityComponent->GetActivityPlayerData(senderId); - if (!data) return; + if (!scriptedActivityComponent->PlayerHasActivityData(senderId)) return; if (args == "course_cancel") { GameMessages::SendNotifyClientObject(selfId, u"cancel_timer", 0, 0, @@ -84,8 +83,11 @@ void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std } else if (args == "course_finish") { const auto raceEndTime = Game::server->GetUptime(); const auto fRaceEndTime = std::chrono::duration<float, std::ratio<1>>(raceEndTime).count(); - const auto raceTimeElapsed = fRaceEndTime - data->values[1]; - data->values[2] = raceTimeElapsed; + const float startTime = scriptedActivityComponent->GetActivityValue(senderId, 1); + if (startTime == 0 || startTime == -1.0f) return; + + const auto raceTimeElapsed = fRaceEndTime - startTime; + scriptedActivityComponent->SetActivityValue(senderId, 2, raceTimeElapsed); auto* const missionComponent = sender->GetComponent<MissionComponent>(); if (missionComponent != nullptr) { diff --git a/dScripts/ActivityManager.cpp b/dScripts/ActivityManager.cpp index 047eb76f..35459538 100644 --- a/dScripts/ActivityManager.cpp +++ b/dScripts/ActivityManager.cpp @@ -114,7 +114,7 @@ uint32_t ActivityManager::CalculateActivityRating(Entity* self, const LWOOBJID p if (sac == nullptr) return 0; - return sac->GetInstance(playerID)->GetParticipants().size(); + return sac->GetInstance(playerID).GetParticipants().size(); } uint32_t ActivityManager::GetActivityID(const Entity* self) { From 9f8d300340388c9048674111206f12f7e81048f2 Mon Sep 17 00:00:00 2001 From: Daniel Seiler <me@xiphoseer.de> Date: Fri, 12 Jun 2026 03:38:38 +0200 Subject: [PATCH 53/69] fix(docker): Unset MAXIMUM_OUTGOING_BANDWIDTH by default (#1548) --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index dbd16603..8e4cb760 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,6 +43,7 @@ services: - MYSQL_PASSWORD=${MARIADB_PASSWORD:?error} - EXTERNAL_IP=${EXTERNAL_IP:-localhost} - CLIENT_NET_VERSION=${CLIENT_NET_VERSION:-171022} + - MAXIMUM_OUTGOING_BANDWIDTH=${MAXIMUM_OUTGOING_BANDWIDTH:0} depends_on: - darkflamedb ports: From 90db1ac6995da866e2cacdf88163cdce8cd1ebb4 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 13 Jun 2026 00:21:53 -0700 Subject: [PATCH 54/69] fix: incorrect network variable being set (#1994) --- dScripts/02_server/Map/AM/AmDrawBridge.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dScripts/02_server/Map/AM/AmDrawBridge.cpp b/dScripts/02_server/Map/AM/AmDrawBridge.cpp index 590024b5..c356145b 100644 --- a/dScripts/02_server/Map/AM/AmDrawBridge.cpp +++ b/dScripts/02_server/Map/AM/AmDrawBridge.cpp @@ -48,7 +48,7 @@ void AmDrawBridge::OnTimerDone(Entity* self, std::string timerName) { } self->SetNetworkVar(u"BridgeLeaving", true); - self->SetVar(u"BridgeDown", false); + self->SetNetworkVar(u"InUse", false); } else if (timerName == "SmashEffectBridge") { self->SetNetworkVar(u"SmashBridge", 5); } else if (timerName == "rotateBridgeDown") { From 0101933f5cae1ba5c09ee54301ceeb462afe0362 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:54:52 -0700 Subject: [PATCH 55/69] chore: cleanup pointer management for LDF data (#1995) * change network settings from vector to LwoNameValue * move settings on Entity to managed memory * Migrate more members * chore: remove pointer leakage from raw ldf pointers * feedback * fix ci --- .github/workflows/build-and-test.yml | 6 +- CMakeLists.txt | 4 - CMakePresets.json | 2 +- dCommon/CMakeLists.txt | 9 +- dCommon/LDFFormat.cpp | 266 +++++++++--------- dCommon/LDFFormat.h | 94 ++++++- dCommon/NiPoint3.h | 4 +- dCommon/NiQuaternion.h | 4 +- dCommon/dEnums/dCommonVars.h | 12 - dGame/Entity.cpp | 43 +-- dGame/Entity.h | 138 ++++----- dGame/LeaderboardManager.cpp | 49 ++-- dGame/LeaderboardManager.h | 5 +- dGame/dComponents/CharacterComponent.cpp | 3 +- dGame/dComponents/DestroyableComponent.cpp | 6 +- dGame/dComponents/InventoryComponent.cpp | 31 +- dGame/dComponents/InventoryComponent.h | 4 +- dGame/dComponents/ModelComponent.cpp | 4 +- dGame/dComponents/PhantomPhysicsComponent.h | 1 - .../PropertyManagementComponent.cpp | 72 ++--- dGame/dComponents/RacingControlComponent.cpp | 18 +- dGame/dComponents/RacingControlComponent.h | 2 +- dGame/dComponents/ScriptComponent.cpp | 10 +- dGame/dComponents/SkillComponent.cpp | 3 +- dGame/dEntity/EntityInfo.h | 4 +- dGame/dGameMessages/GameMessages.cpp | 59 ++-- dGame/dGameMessages/GameMessages.h | 7 +- dGame/dInventory/EquippedItem.h | 2 +- dGame/dInventory/Item.cpp | 26 +- dGame/dInventory/Item.h | 10 +- dGame/dMission/MissionTask.cpp | 1 - .../SlashCommands/DEVGMCommands.cpp | 10 +- dGame/dUtilities/VanityUtilities.cpp | 16 +- dGame/dUtilities/VanityUtilities.h | 2 +- dNet/WorldPackets.cpp | 23 +- .../02_server/Enemy/AM/AmDarklingDragon.cpp | 15 +- .../02_server/Enemy/FV/FvMaelstromDragon.cpp | 15 +- .../02_server/Enemy/General/BaseEnemyApe.cpp | 11 +- .../02_server/Enemy/General/BaseEnemyMech.cpp | 5 +- .../Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp | 4 +- dScripts/02_server/Map/AM/AmSkullkinTower.cpp | 4 +- .../02_server/Map/General/PetDigServer.cpp | 10 +- dScripts/02_server/Map/General/QbSpawner.cpp | 14 +- .../Map/NS/NsConcertChoiceBuildManager.cpp | 10 +- .../Map/NT/NtCombatChallengeServer.cpp | 2 +- .../Map/Property/AG_Small/ZoneAgProperty.cpp | 6 +- .../boss_instance/NjMonastryBossInstance.cpp | 4 +- .../02_server/Objects/StinkyFishTarget.cpp | 4 +- dScripts/SpawnPetBaseServer.cpp | 10 +- dScripts/ai/FV/FvPandaSpawnerServer.cpp | 6 +- dScripts/ai/GF/GfBanana.cpp | 2 +- dScripts/ai/GF/PetDigBuild.cpp | 8 +- .../ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp | 23 +- dScripts/ai/NS/WH/RockHydrantSmashable.cpp | 4 +- dScripts/ai/PETS/HydrantSmashable.cpp | 4 +- dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp | 68 +++-- dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp | 68 +++-- dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp | 68 +++-- .../TRACK_NS_WINTER/NsWinterRaceServer.cpp | 68 +++-- dZoneManager/Level.cpp | 30 +- dZoneManager/Level.h | 2 +- dZoneManager/Spawner.h | 2 +- dZoneManager/Zone.cpp | 4 +- dZoneManager/Zone.h | 2 +- dZoneManager/dZMCommon.h | 3 +- thirdparty/CMakeLists.txt | 2 +- thirdparty/recastnavigation | 2 +- 67 files changed, 676 insertions(+), 754 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c26f0dee..3963fdbb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -34,12 +34,12 @@ jobs: if: ${{ matrix.os == 'windows-2025' }} uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3 with: - vs-version: '[17,18)' + vs-version: '[18,19)' msbuild-architecture: x64 - - name: Get CMake 3.x + - name: Get CMake uses: lukka/get-cmake@591817e96fcad43505fb4eae36172462abb3a42e # v4.3.3 with: - cmakeVersion: "~3.25.0" + cmakeVersion: "latest" - name: cmake uses: lukka/run-cmake@5d55ea7949e25f69f0ecb516d8d572297e03a956 # v10.9 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 76930398..63786950 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,10 +16,6 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0") - set(CMAKE_POLICY_VERSION_MINIMUM 3.5) -endif() - set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debugging set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions diff --git a/CMakePresets.json b/CMakePresets.json index 2819f320..bc6c8dad 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -49,7 +49,7 @@ "inherits": "default", "displayName": "[Multi] Windows (MSVC)", "description": "Set architecture to 64-bit (b/c RakNet)", - "generator": "Visual Studio 17 2022", + "generator": "Visual Studio 18 2026", "binaryDir": "${sourceDir}/build/msvc", "architecture": { "value": "x64" diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index ba67974e..0e7c7d52 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -49,11 +49,10 @@ if (UNIX) elseif (WIN32) include(FetchContent) - # TODO Keep an eye on the zlib repository for an update to disable testing. Don't forget to update CMakePresets FetchContent_Declare( zlib - URL https://github.com/madler/zlib/archive/refs/tags/v1.2.11.zip - URL_HASH MD5=9d6a627693163bbbf3f26403a3a0b0b1 + URL https://github.com/madler/zlib/archive/refs/tags/v1.3.2.zip + URL_HASH MD5=adbba6eef8960c3412818b2e241f46dc GIT_PROGRESS TRUE GIT_SHALLOW 1 ) @@ -62,12 +61,12 @@ elseif (WIN32) set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) # Disable warning about the minimum version of cmake used for bcrypt being deprecated in the future set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE) + # Disable zlib tests + set(ZLIB_BUILD_TESTING OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(zlib) set(ZLIB_INCLUDE_DIRS ${zlib_SOURCE_DIR} ${zlib_BINARY_DIR}) - set_target_properties(zlib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIRS}") - add_library(ZLIB::ZLIB ALIAS zlib) else () message( FATAL_ERROR diff --git a/dCommon/LDFFormat.cpp b/dCommon/LDFFormat.cpp index da28ae6e..04ba643a 100644 --- a/dCommon/LDFFormat.cpp +++ b/dCommon/LDFFormat.cpp @@ -10,163 +10,151 @@ #include <string_view> #include <vector> -using LDFKey = std::string_view; -using LDFTypeAndValue = std::string_view; - -using LDFType = std::string_view; -using LDFValue = std::string_view; - //! Returns a pointer to a LDFData value based on string format -LDFBaseData* LDFBaseData::DataFromString(const std::string_view& format) { +std::unique_ptr<LDFBaseData> LDFBaseData::DataFromString(const std::string_view& format) { + std::unique_ptr<LDFBaseData> toReturn; // A valid LDF must be at least 3 characters long (=0:) is the shortest valid LDF (empty UTF-16 key with no initial value) - if (format.empty() || format.length() <= 2) return nullptr; - auto equalsPosition = format.find('='); - // You can have an empty key, just make sure the type and value might exist - if (equalsPosition == std::string::npos || equalsPosition == (format.size() - 1)) return nullptr; + if (!format.empty() && format.length() > 2) { + auto equalsPosition = format.find('='); + // You can have an empty key, just make sure the type and value might exist + if (equalsPosition != std::string::npos && equalsPosition != (format.size() - 1)) { - std::pair<LDFKey, LDFTypeAndValue> keyValue; - keyValue.first = format.substr(0, equalsPosition); - keyValue.second = format.substr(equalsPosition + 1, format.size()); + const std::string_view keyValue = format.substr(0, equalsPosition); + const std::string_view typeAndValue = format.substr(equalsPosition + 1, format.size()); - std::u16string key = GeneralUtils::ASCIIToUTF16(keyValue.first); + const auto key = GeneralUtils::ASCIIToUTF16(keyValue); - auto colonPosition = keyValue.second.find(':'); + const auto colonPosition = typeAndValue.find(':'); - // If : is the first thing after an =, then this is an invalid LDF since - // we dont have a type to use. - if (colonPosition == std::string::npos || colonPosition == 0) return nullptr; + // If : is the first thing after an =, then this is an invalid LDF since + // we dont have a type to use. + if (colonPosition != std::string::npos && colonPosition != 0) { + const std::string_view ldfType = typeAndValue.substr(0, colonPosition); + const std::string_view ldfValue = typeAndValue.substr(colonPosition + 1, typeAndValue.size()); - std::pair<LDFType, LDFValue> ldfTypeAndValue; - ldfTypeAndValue.first = keyValue.second.substr(0, colonPosition); - ldfTypeAndValue.second = keyValue.second.substr(colonPosition + 1, keyValue.second.size()); + // Only allow empty values for string values. + if (!ldfValue.empty() || (ldfType == "0" /* UTF-16 */ || ldfType == "13" /* UTF-8 */)) { + const eLDFType type = GeneralUtils::TryParse<eLDFType>(ldfType, LDF_TYPE_UNKNOWN); + switch (type) { + case LDF_TYPE_UTF_16: { + std::u16string data = GeneralUtils::UTF8ToUTF16(ldfValue); + toReturn.reset(new LDFData<std::u16string>(key, data)); + break; + } - // Only allow empty values for string values. - if (ldfTypeAndValue.second.size() == 0 && !(ldfTypeAndValue.first == "0" || ldfTypeAndValue.first == "13")) return nullptr; + case LDF_TYPE_S32: { + const auto data = GeneralUtils::TryParse<int32_t>(ldfValue); + if (data) { + toReturn.reset(new LDFData<int32_t>(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid int32 value (%s) from string (%s)", ldfValue.data(), format.data()); + } - eLDFType type; - char* storage; - try { - type = static_cast<eLDFType>(strtol(ldfTypeAndValue.first.data(), &storage, 10)); - } catch (std::exception) { - LOG("Attempted to process invalid ldf type (%s) from string (%s)", ldfTypeAndValue.first.data(), format.data()); - return nullptr; - } + break; + } - LDFBaseData* returnValue = nullptr; - switch (type) { - case LDF_TYPE_UTF_16: { - std::u16string data = GeneralUtils::UTF8ToUTF16(ldfTypeAndValue.second); - returnValue = new LDFData<std::u16string>(key, data); - break; - } + case LDF_TYPE_FLOAT: { + const auto data = GeneralUtils::TryParse<float>(ldfValue); + if (data) { + toReturn.reset(new LDFData<float>(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid float value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } - case LDF_TYPE_S32: { - const auto data = GeneralUtils::TryParse<int32_t>(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid int32 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData<int32_t>(key, data.value()); + case LDF_TYPE_DOUBLE: { + const auto data = GeneralUtils::TryParse<double>(ldfValue); + if (data) { + toReturn.reset(new LDFData<double>(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid double value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } - break; - } + case LDF_TYPE_U32: + { + uint32_t data; + bool parsed = true; + // Have to do this really weird parsing to allow for copy ellision + if (ldfValue == "true") { + data = 1; + } else if (ldfValue == "false") { + data = 0; + } else { + const auto dataOptional = GeneralUtils::TryParse<uint32_t>(ldfValue); + if (!dataOptional) { + LOG("Warning: Attempted to process invalid uint32 value (%s) from string (%s)", ldfValue.data(), format.data()); + parsed = false; + } else { + data = dataOptional.value(); + } + } - case LDF_TYPE_FLOAT: { - const auto data = GeneralUtils::TryParse<float>(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid float value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData<float>(key, data.value()); - break; - } + if (parsed) toReturn.reset(new LDFData<uint32_t>(key, data)); + break; + } - case LDF_TYPE_DOUBLE: { - const auto data = GeneralUtils::TryParse<double>(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid double value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData<double>(key, data.value()); - break; - } + case LDF_TYPE_BOOLEAN: { + bool data; + bool parsed = true; + // Have to do this really weird parsing to allow for copy ellision + if (ldfValue == "true") { + data = true; + } else if (ldfValue == "false") { + data = false; + } else { + const auto dataOptional = GeneralUtils::TryParse<bool>(ldfValue); + if (!dataOptional) { + LOG("Warning: Attempted to process invalid bool value (%s) from string (%s)", ldfValue.data(), format.data()); + parsed = false; + } else { + data = dataOptional.value(); + } + } - case LDF_TYPE_U32: - { - uint32_t data; + if (parsed) toReturn.reset(new LDFData<bool>(key, data)); + break; + } - if (ldfTypeAndValue.second == "true") { - data = 1; - } else if (ldfTypeAndValue.second == "false") { - data = 0; - } else { - const auto dataOptional = GeneralUtils::TryParse<uint32_t>(ldfTypeAndValue.second); - if (!dataOptional) { - LOG("Warning: Attempted to process invalid uint32 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; + case LDF_TYPE_U64: { + const auto data = GeneralUtils::TryParse<uint64_t>(ldfValue); + if (data) { + toReturn.reset(new LDFData<uint64_t>(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid uint64 value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } + + case LDF_TYPE_OBJID: { + const auto data = GeneralUtils::TryParse<LWOOBJID>(ldfValue); + if (data) { + toReturn.reset(new LDFData<LWOOBJID>(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid LWOOBJID value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } + + case LDF_TYPE_UTF_8: { + toReturn.reset(new LDFData<std::string>(key, ldfValue.data())); + break; + } + + case LDF_TYPE_UNKNOWN: + [[fallthrough]]; + default: { + LOG("Warning: Attempted to process invalid unknown value (%s) from string (%s)", ldfValue.data(), format.data()); + break; + } + + } + } } - data = dataOptional.value(); } - - returnValue = new LDFData<uint32_t>(key, data); - break; } - case LDF_TYPE_BOOLEAN: { - bool data; - - if (ldfTypeAndValue.second == "true") { - data = true; - } else if (ldfTypeAndValue.second == "false") { - data = false; - } else { - const auto dataOptional = GeneralUtils::TryParse<bool>(ldfTypeAndValue.second); - if (!dataOptional) { - LOG("Warning: Attempted to process invalid bool value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - data = dataOptional.value(); - } - - returnValue = new LDFData<bool>(key, data); - break; - } - - case LDF_TYPE_U64: { - const auto data = GeneralUtils::TryParse<uint64_t>(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid uint64 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData<uint64_t>(key, data.value()); - break; - } - - case LDF_TYPE_OBJID: { - const auto data = GeneralUtils::TryParse<LWOOBJID>(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid LWOOBJID value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData<LWOOBJID>(key, data.value()); - break; - } - - case LDF_TYPE_UTF_8: { - std::string data = ldfTypeAndValue.second.data(); - returnValue = new LDFData<std::string>(key, data); - break; - } - - case LDF_TYPE_UNKNOWN: { - LOG("Warning: Attempted to process invalid unknown value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - break; - } - - default: { - LOG("Warning: Attempted to process invalid LDF type (%d) from string (%s)", type, format.data()); - break; - } - } - return returnValue; + return toReturn; } diff --git a/dCommon/LDFFormat.h b/dCommon/LDFFormat.h index 7c2e939b..81a6648c 100644 --- a/dCommon/LDFFormat.h +++ b/dCommon/LDFFormat.h @@ -1,11 +1,12 @@ -#ifndef __LDFFORMAT__H__ -#define __LDFFORMAT__H__ +#ifndef LDFFORMAT_H +#define LDFFORMAT_H // Custom Classes #include "dCommonVars.h" #include "GeneralUtils.h" // C++ +#include <map> #include <string> #include <string_view> #include <sstream> @@ -46,17 +47,17 @@ public: virtual std::string GetValueAsString() const = 0; - virtual LDFBaseData* Copy() const = 0; + virtual std::unique_ptr<LDFBaseData> Copy() const = 0; /** * Given an input string, return the data as a LDF key. */ - static LDFBaseData* DataFromString(const std::string_view& format); + static std::unique_ptr<LDFBaseData> DataFromString(const std::string_view& format); }; template<typename T> -class LDFData: public LDFBaseData { +class LDFData : public LDFBaseData { private: std::u16string key; T value; @@ -164,8 +165,8 @@ public: return this->GetValueString(); } - LDFBaseData* Copy() const override { - return new LDFData<T>(key, value); + std::unique_ptr<LDFBaseData> Copy() const override { + return std::make_unique<LDFData<T>>(key, value); } inline static const T Default = {}; @@ -226,4 +227,81 @@ template<> inline std::string LDFData<LWOOBJID>::GetValueString() const { return template<> inline std::string LDFData<std::string>::GetValueString() const { return this->value; } -#endif //!__LDFFORMAT__H__ +struct LwoNameValue { + using LDFPtr = std::unique_ptr<LDFBaseData>; + using ValueType = std::map<std::u16string, LDFPtr>; + + LwoNameValue& operator=(const LwoNameValue& other) { + this->values = other.Copy(); + return *this; + } + + template<typename T> + void Insert(const std::u16string& key, const T& value) { + this->values.insert_or_assign(key, std::unique_ptr(std::make_unique<LDFData<T>>(key, value))); + } + + void Insert(const std::u16string& key, const char* value) { + this->Insert<std::string>(key, value); + } + + void Insert(const std::u16string& key, const char16_t* value) { + this->Insert<std::u16string>(key, value); + } + + template<typename T> + void Insert(const std::string& key, const T& value) { + this->Insert<T>(GeneralUtils::UTF8ToUTF16(key), value); + } + + void Insert(const std::string& key, const char* value) { + this->Insert<std::string>(GeneralUtils::UTF8ToUTF16(key), value); + } + + void Insert(const std::string& key, const char16_t* value) { + this->Insert<std::u16string>(GeneralUtils::UTF8ToUTF16(key), value); + } + + const LDFPtr& ParseInsert(const std::string& data) { + LDFPtr toInsert(LDFBaseData::DataFromString(data)); + return toInsert ? + this->values.insert_or_assign(toInsert->GetKey(), std::move(toInsert)).first->second : + this->values.insert_or_assign(u"FAILED_TO_PARSE_" + GeneralUtils::UTF8ToUTF16(data), std::make_unique<LDFData<std::string>>("", "")).first->second; + } + + const LDFPtr& ParseInsert(const std::u16string& data) { + return this->ParseInsert(GeneralUtils::UTF16ToWTF8(data)); + } + + ValueType::const_iterator begin() const { + return this->values.cbegin(); + } + + ValueType::const_iterator end() const { + return this->values.cend(); + } + + void Erase(const std::u16string& key) { + this->values.erase(key); + } + + void Erase(const std::string& key) { + this->Erase(GeneralUtils::ASCIIToUTF16(key)); + } + + LwoNameValue() = default; + + LwoNameValue(const LwoNameValue& other) { + this->values = other.Copy(); + } + + ValueType values; +private: + ValueType Copy() const { + ValueType copy; + for (const auto& [key, value] : this->values) copy.insert_or_assign(key, value->Copy()); + return copy; + } +}; + +#endif //!LDFFORMAT_H diff --git a/dCommon/NiPoint3.h b/dCommon/NiPoint3.h index b968a4de..d1b65c93 100644 --- a/dCommon/NiPoint3.h +++ b/dCommon/NiPoint3.h @@ -1,6 +1,8 @@ #ifndef __NIPOINT3_H__ #define __NIPOINT3_H__ - +#ifndef GLM_ENABLE_EXPERIMENTAL +# define GLM_ENABLE_EXPERIMENTAL +#endif /*! \file NiPoint3.hpp \brief Defines a point in space in XYZ coordinates diff --git a/dCommon/NiQuaternion.h b/dCommon/NiQuaternion.h index a4546e2c..c4ef0661 100644 --- a/dCommon/NiQuaternion.h +++ b/dCommon/NiQuaternion.h @@ -1,6 +1,8 @@ #ifndef NIQUATERNION_H #define NIQUATERNION_H - +#ifndef GLM_ENABLE_EXPERIMENTAL +# define GLM_ENABLE_EXPERIMENTAL +#endif // Custom Classes #include "NiPoint3.h" diff --git a/dCommon/dEnums/dCommonVars.h b/dCommon/dEnums/dCommonVars.h index fc1ccded..c3ed1b14 100644 --- a/dCommon/dEnums/dCommonVars.h +++ b/dCommon/dEnums/dCommonVars.h @@ -111,18 +111,6 @@ private: constexpr LWOSCENEID LWOSCENEID_INVALID = -1; -struct LWONameValue { - uint32_t length = 0; //!< The length of the name - std::u16string name; //!< The name - - LWONameValue() = default; - - LWONameValue(const std::u16string& name) { - this->name = name; - this->length = static_cast<uint32_t>(name.length()); - } -}; - struct FriendData { public: bool isOnline = false; diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index d88aa1d5..25ffb1e4 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -949,13 +949,13 @@ void Entity::SetGMLevel(eGameMasterLevel value) { } } -void Entity::WriteLDFData(const std::vector<LDFBaseData*>& ldf, RakNet::BitStream& outBitStream) const { +void Entity::WriteLDFData(const LwoNameValue& ldf, RakNet::BitStream& outBitStream) const { RakNet::BitStream settingStream; - int32_t numberOfValidKeys = ldf.size(); + int32_t numberOfValidKeys = ldf.values.size(); // Writing keys value pairs the client does not expect to receive or interpret will result in undefined behavior, // so we need to filter out any keys that are not valid and fix the number of valid keys to be correct. - for (LDFBaseData* data : ldf) { + for (const auto& data : ldf.values | std::views::values) { if (data && data->GetValueType() != eLDFType::LDF_TYPE_UNKNOWN) { data->WriteToPacket(settingStream); } else { @@ -987,16 +987,16 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke const auto& syncLDF = GetVar<std::vector<std::u16string>>(u"syncLDF"); // Only sync for models. - if (!m_Settings.empty() && (GetComponent<ModelComponent>() && !GetComponent<PetComponent>())) { + if (!m_Settings.values.empty() && (GetComponent<ModelComponent>() && !GetComponent<PetComponent>())) { outBitStream.Write1(); // Has ldf data WriteLDFData(m_Settings, outBitStream); } else if (!syncLDF.empty()) { // Find all the ldf data we need to write - std::vector<LDFBaseData*> ldfData; - ldfData.reserve(m_Settings.size()); + LwoNameValue ldfData; for (const auto& data : syncLDF) { - ldfData.push_back(GetVarData(data)); + const auto* toInsert = GetVarData(data); + if (toInsert) ldfData.values.insert_or_assign(data, toInsert->Copy()); } outBitStream.Write1(); // Has ldf data @@ -2045,13 +2045,7 @@ void Entity::SetI64(const std::u16string& name, const int64_t value) { } bool Entity::HasVar(const std::u16string& name) const { - for (auto* data : m_Settings) { - if (data->GetKey() == name) { - return true; - } - } - - return false; + return m_Settings.values.contains(name); } uint16_t Entity::GetNetworkId() const { @@ -2083,24 +2077,13 @@ void Entity::SendNetworkVar(const std::string& data, const SystemAddress& sysAdd GameMessages::SendSetNetworkScriptVar(this, sysAddr, data); } -LDFBaseData* Entity::GetVarData(const std::u16string& name) const { - for (auto* data : m_Settings) { - if (data == nullptr) { - continue; - } - - if (data->GetKey() != name) { - continue; - } - - return data; - } - - return nullptr; +const LDFBaseData* const Entity::GetVarData(const std::u16string& name) const { + const auto itr = m_Settings.values.find(name); + return itr != m_Settings.values.cend() ? itr->second.get() : nullptr; } std::string Entity::GetVarAsString(const std::u16string& name) const { - auto* data = GetVarData(name); + const auto* const data = GetVarData(name); return data ? data->GetValueAsString() : ""; } @@ -2276,7 +2259,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& r } auto& configData = objectInfo.PushDebug("Config Data"); - for (const auto config : m_Settings) { + for (const auto& config : m_Settings.values | std::views::values) { configData.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString(); } diff --git a/dGame/Entity.h b/dGame/Entity.h index c07bd3f4..a8781d42 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -97,9 +97,9 @@ public: LWOOBJID GetSpawnerID() const { return m_SpawnerID; } - const std::vector<LDFBaseData*>& GetSettings() const { return m_Settings; } + const LwoNameValue& GetSettings() const { return m_Settings; } - const std::vector<LDFBaseData*>& GetNetworkSettings() const { return m_NetworkSettings; } + const LwoNameValue& GetNetworkSettings() const { return m_NetworkSettings; } bool GetIsDead() const; @@ -312,6 +312,12 @@ public: template<typename T> void SetNetworkVar(const std::u16string& name, std::vector<T> value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); + template<typename T> + void SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); + + template<typename T> + LwoNameValue::ValueType::iterator InsertNetworkVar(const std::u16string& name, T value); + template<typename T> T GetNetworkVar(const std::u16string& name); @@ -324,11 +330,6 @@ public: template<typename ComponentType, typename... VaArgs> ComponentType* AddComponent(VaArgs... args); - /** - * Get the LDF data. - */ - LDFBaseData* GetVarData(const std::u16string& name) const; - /** * Get the LDF value and convert it to a string. */ @@ -360,7 +361,7 @@ public: // This is the actual function that will be registered, which casts the base GameMsg to the derived type const auto castWrapper = [boundFunction](GameMessages::GameMsg& msg) { return boundFunction(static_cast<DerivedGameMsg&>(msg)); - }; + }; DerivedGameMsg msg; RegisterMsg(msg.msgId, castWrapper); } @@ -371,13 +372,20 @@ public: static Observable<Entity*, const PositionUpdate&> OnPlayerPositionUpdate; private: - void WriteLDFData(const std::vector<LDFBaseData*>& ldf, RakNet::BitStream& outBitStream) const; + + /** + * Get the LDF data. + */ + const LDFBaseData* const GetVarData(const std::u16string& name) const; + template<typename T> + LwoNameValue::ValueType::iterator InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value); + void WriteLDFData(const LwoNameValue& ldf, RakNet::BitStream& outBitStream) const; LWOOBJID m_ObjectID; LOT m_TemplateID; - std::vector<LDFBaseData*> m_Settings; - std::vector<LDFBaseData*> m_NetworkSettings; + LwoNameValue m_Settings; + LwoNameValue m_NetworkSettings; NiPoint3 m_DefaultPosition; NiQuaternion m_DefaultRotation = QuatUtils::IDENTITY; @@ -459,13 +467,13 @@ T* Entity::GetComponent() const { template<typename T> const T& Entity::GetVar(const std::u16string& name) const { - auto* data = GetVarData(name); + const auto* const data = GetVarData(name); if (data == nullptr) { return LDFData<T>::Default; } - auto* typed = dynamic_cast<LDFData<T>*>(data); + auto* typed = dynamic_cast<const LDFData<T>* const>(data); if (typed == nullptr) { return LDFData<T>::Default; @@ -483,52 +491,44 @@ T Entity::GetVarAs(const std::u16string& name) const { template<typename T> void Entity::SetVar(const std::u16string& name, T value) { - auto* data = GetVarData(name); + InsertLnvData<T>(m_Settings, name, value); +} - if (data == nullptr) { - auto* data = new LDFData<T>(name, value); - - m_Settings.push_back(data); - - return; +template<typename T> +LwoNameValue::ValueType::iterator Entity::InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value) { + auto itr = lnv.values.find(key); + if (itr != lnv.values.end()) { + auto* lnvCast = dynamic_cast<LDFData<T>*>(itr->second.get()); + if (!lnvCast) { + // Is of different type + itr->second = std::make_unique<LDFData<T>>(key, value); + } else { + // Is the same type and exists + lnvCast->SetValue(value); + } + } else { + // Doesn't exist + itr = lnv.values.insert_or_assign(key, std::make_unique<LDFData<T>>(key, value)).first; } - auto* typed = dynamic_cast<LDFData<T>*>(data); + return itr; +} - if (typed == nullptr) { - return; - } - - typed->SetValue(value); +template<typename T> +LwoNameValue::ValueType::iterator Entity::InsertNetworkVar(const std::u16string& name, T value) { + return InsertLnvData<T>(m_NetworkSettings, name, value); } template<typename T> void Entity::SetNetworkVar(const std::u16string& name, T value, const SystemAddress& sysAddr) { - LDFData<T>* newData = nullptr; + const auto itr = InsertNetworkVar<T>(name, value); - for (auto* data : m_NetworkSettings) { - if (data->GetKey() != name) - continue; + SendNetworkVar(itr->second->GetString(), sysAddr); +} - newData = dynamic_cast<LDFData<T>*>(data); - if (newData != nullptr) { - newData->SetValue(value); - } else { // If we're changing types - m_NetworkSettings.erase( - std::remove(m_NetworkSettings.begin(), m_NetworkSettings.end(), data), m_NetworkSettings.end() - ); - delete data; - } - - break; - } - - if (newData == nullptr) { - newData = new LDFData<T>(name, value); - } - - m_NetworkSettings.push_back(newData); - SendNetworkVar(newData->GetString(true), sysAddr); +template<typename T> +void Entity::SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr) { + SetNetworkVar(GeneralUtils::UTF8ToUTF16(name), value, sysAddr); } template<typename T> @@ -537,28 +537,11 @@ void Entity::SetNetworkVar(const std::u16string& name, std::vector<T> values, co auto index = 1; for (const auto& value : values) { - LDFData<T>* newData = nullptr; const auto& indexedName = name + u"." + GeneralUtils::to_u16string(index); - - for (auto* data : m_NetworkSettings) { - if (data->GetKey() != indexedName) - continue; - - newData = dynamic_cast<LDFData<T>*>(data); - newData->SetValue(value); - break; - } - - if (newData == nullptr) { - newData = new LDFData<T>(indexedName, value); - } - - m_NetworkSettings.push_back(newData); - - if (index == values.size()) { - updates << newData->GetString(true); - } else { - updates << newData->GetString(true) << "\n"; + const auto itr = InsertNetworkVar<T>(indexedName, value); + updates << itr->second->GetString(); + if (index != values.size()) { + updates << "\n"; } index++; @@ -569,18 +552,15 @@ void Entity::SetNetworkVar(const std::u16string& name, std::vector<T> values, co template<typename T> T Entity::GetNetworkVar(const std::u16string& name) { - for (auto* data : m_NetworkSettings) { - if (data == nullptr || data->GetKey() != name) - continue; + T toReturn = LDFData<T>::Default; - auto* typed = dynamic_cast<LDFData<T>*>(data); - if (typed == nullptr) - continue; - - return typed->GetValue(); + const auto itr = m_NetworkSettings.values.find(name); + if (itr != m_NetworkSettings.values.cend()) { + auto* cast = dynamic_cast<LDFData<T>*>(itr->second.get()); + if (cast) toReturn = cast->GetValue(); } - return LDFData<T>::Default; + return toReturn; } /** diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp index b8e40154..a7ca59c1 100644 --- a/dGame/LeaderboardManager.cpp +++ b/dGame/LeaderboardManager.cpp @@ -39,10 +39,10 @@ Leaderboard::~Leaderboard() { } void Leaderboard::Clear() { - for (auto& entry : entries) for (auto ldfData : entry) delete ldfData; + entries.clear(); } -inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, LDFBaseData* data) { +inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, const std::unique_ptr<LDFBaseData>& data) { leaderboard << "\nResult[0].Row[" << index << "]." << data->GetString(); } @@ -59,8 +59,8 @@ void Leaderboard::Serialize(RakNet::BitStream& bitStream) const { int32_t rowNumber = 0; for (auto& entry : entries) { - for (auto* data : entry) { - WriteLeaderboardRow(leaderboard, rowNumber, data); + for (const auto& data : entry.values | std::views::values) { + if (data) WriteLeaderboardRow(leaderboard, rowNumber, data); } rowNumber++; } @@ -85,57 +85,56 @@ void QueryToLdf(Leaderboard& leaderboard, const std::vector<ILeaderboard::Entry> for (const auto& leaderboardEntry : leaderboardEntries) { constexpr int32_t MAX_NUM_DATA_PER_ROW = 9; auto& entry = leaderboard.PushBackEntry(); - entry.reserve(MAX_NUM_DATA_PER_ROW); - entry.push_back(new LDFData<uint64_t>(u"CharacterID", leaderboardEntry.charId)); - entry.push_back(new LDFData<uint64_t>(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp)); - entry.push_back(new LDFData<int32_t>(u"NumPlayed", leaderboardEntry.numTimesPlayed)); - entry.push_back(new LDFData<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name))); - entry.push_back(new LDFData<uint64_t>(u"RowNumber", leaderboardEntry.ranking)); + entry.Insert<uint64_t>(u"CharacterID", leaderboardEntry.charId); + entry.Insert<uint64_t>(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp); + entry.Insert<int32_t>(u"NumPlayed", leaderboardEntry.numTimesPlayed); + entry.Insert<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name)); + entry.Insert<uint64_t>(u"RowNumber", leaderboardEntry.ranking); switch (leaderboard.GetLeaderboardType()) { case ShootingGallery: - entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore)); + entry.Insert<int32_t>(u"Score", leaderboardEntry.primaryScore); // Score:1 - entry.push_back(new LDFData<int32_t>(u"Streak", leaderboardEntry.secondaryScore)); + entry.Insert<int32_t>(u"Streak", leaderboardEntry.secondaryScore); // Streak:1 - entry.push_back(new LDFData<float>(u"HitPercentage", leaderboardEntry.tertiaryScore)); + entry.Insert<float>(u"HitPercentage", leaderboardEntry.tertiaryScore); // HitPercentage:3 between 0 and 1 break; case Racing: - entry.push_back(new LDFData<float>(u"BestTime", leaderboardEntry.primaryScore)); + entry.Insert<float>(u"BestTime", leaderboardEntry.primaryScore); // BestLapTime:3 - entry.push_back(new LDFData<float>(u"BestLapTime", leaderboardEntry.secondaryScore)); + entry.Insert<float>(u"BestLapTime", leaderboardEntry.secondaryScore); // BestTime:3 - entry.push_back(new LDFData<int32_t>(u"License", 1)); + entry.Insert<int32_t>(u"License", 1); // License:1 - 1 if player has completed mission 637 and 0 otherwise - entry.push_back(new LDFData<int32_t>(u"NumWins", leaderboardEntry.numWins)); + entry.Insert<int32_t>(u"NumWins", leaderboardEntry.numWins); // NumWins:1 break; case UnusedLeaderboard4: - entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore)); + entry.Insert<int32_t>(u"Points", leaderboardEntry.primaryScore); // Points:1 break; case MonumentRace: - entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore)); + entry.Insert<int32_t>(u"Time", leaderboardEntry.primaryScore); // Time:1(?) break; case FootRace: - entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore)); + entry.Insert<int32_t>(u"Time", leaderboardEntry.primaryScore); // Time:1 break; case Survival: - entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore)); + entry.Insert<int32_t>(u"Points", leaderboardEntry.primaryScore); // Points:1 - entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore)); + entry.Insert<int32_t>(u"Time", leaderboardEntry.secondaryScore); // Time:1 break; case SurvivalNS: - entry.push_back(new LDFData<int32_t>(u"Wave", leaderboardEntry.primaryScore)); + entry.Insert<int32_t>(u"Wave", leaderboardEntry.primaryScore); // Wave:1 - entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore)); + entry.Insert<int32_t>(u"Time", leaderboardEntry.secondaryScore); // Time:1 break; case Donations: - entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore)); + entry.Insert<int32_t>(u"Score", leaderboardEntry.primaryScore); // Score:1 break; case None: diff --git a/dGame/LeaderboardManager.h b/dGame/LeaderboardManager.h index 760d282e..c99634c1 100644 --- a/dGame/LeaderboardManager.h +++ b/dGame/LeaderboardManager.h @@ -70,8 +70,7 @@ public: private: - using LeaderboardEntry = std::vector<LDFBaseData*>; - using LeaderboardEntries = std::vector<LeaderboardEntry>; + using LeaderboardEntries = std::vector<LwoNameValue>; LeaderboardEntries entries; LWOOBJID relatedPlayer; @@ -81,7 +80,7 @@ private: bool weekly; uint32_t numResults; public: - LeaderboardEntry& PushBackEntry() { + LwoNameValue& PushBackEntry() { return entries.emplace_back(); } diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index a703d884..0c5230f8 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -24,6 +24,7 @@ #include "WorldPackets.h" #include "MessageType/Game.h" #include <ctime> +#include <ranges> CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID, Character* character, const SystemAddress& systemAddress) : Component(parent, componentID) { m_Character = character; @@ -491,7 +492,7 @@ Item* CharacterComponent::RocketEquip(Entity* player) { if (!rocket) return rocket; // build and define the rocket config - for (LDFBaseData* data : rocket->GetConfig()) { + for (const auto& data : rocket->GetConfig().values | std::views::values) { if (data->GetKey() == u"assemblyPartLOTs") { std::string newRocketStr = data->GetValueAsString() + ";"; GeneralUtils::ReplaceInString(newRocketStr, "+", ";"); diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index ebccc662..90591367 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -881,9 +881,9 @@ void DestroyableComponent::FixStats() { int32_t currentImagination = destroyableComponent->GetImagination(); // Unequip all items - auto equipped = inventoryComponent->GetEquippedItems(); + const auto equipped = inventoryComponent->GetEquippedItems(); - for (auto& equippedItem : equipped) { + for (const auto& equippedItem : equipped) { // Get the item with the item ID auto* item = inventoryComponent->FindItemById(equippedItem.second.id); @@ -924,7 +924,7 @@ void DestroyableComponent::FixStats() { buffComponent->ReApplyBuffs(); // Requip all items - for (auto& equippedItem : equipped) { + for (const auto& equippedItem : equipped) { // Get the item with the item ID auto* item = inventoryComponent->FindItemById(equippedItem.second.id); diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index a7e8d5ed..2f3436fe 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -173,7 +173,7 @@ void InventoryComponent::AddItem( const uint32_t count, eLootSourceType lootSourceType, eInventoryType inventoryType, - const std::vector<LDFBaseData*>& config, + const LwoNameValue& config, const LWOOBJID parent, const bool showFlyingLoot, bool isModMoveAndEquip, @@ -204,7 +204,7 @@ void InventoryComponent::AddItem( auto* inventory = GetInventory(inventoryType); - if (!config.empty() || bound) { + if (!config.values.empty() || bound) { const auto slot = preferredSlot != -1 && inventory->IsSlotEmpty(preferredSlot) ? preferredSlot : inventory->FindEmptySlot(); if (slot == -1) { @@ -356,7 +356,7 @@ void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType in const auto subkey = item->GetSubKey(); - if (subkey == LWOOBJID_EMPTY && item->GetConfig().empty() && (!item->GetBound() || (item->GetBound() && item->GetInfo().isBOP))) { + if (subkey == LWOOBJID_EMPTY && item->GetConfig().values.empty() && (!item->GetBound() || (item->GetBound() && item->GetInfo().isBOP))) { auto left = std::min<uint32_t>(count, origin->GetLotCount(lot)); while (left > 0) { @@ -379,11 +379,7 @@ void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType in isModMoveAndEquip = false; } } else { - std::vector<LDFBaseData*> config; - - for (auto* const data : item->GetConfig()) { - config.push_back(data->Copy()); - } + const auto config = item->GetConfig(); const auto delta = std::min<uint32_t>(item->GetCount(), count); @@ -744,18 +740,17 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b outBitStream.Write0(); - bool flag = !item.config.empty(); + bool flag = !item.config.values.empty(); outBitStream.Write(flag); if (flag) { RakNet::BitStream ldfStream; - ldfStream.Write<int32_t>(item.config.size()); // Key count - for (LDFBaseData* data : item.config) { + ldfStream.Write<int32_t>(item.config.values.size()); // Key count + for (const auto& data : item.config.values | std::views::values) { if (data->GetKey() == u"assemblyPartLOTs") { std::string newRocketStr = data->GetValueAsString() + ";"; GeneralUtils::ReplaceInString(newRocketStr, "+", ";"); - LDFData<std::u16string>* ldf_data = new LDFData<std::u16string>(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(newRocketStr)); - ldf_data->WriteToPacket(ldfStream); - delete ldf_data; + LDFData<std::u16string> ldf_data(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(newRocketStr)); + ldf_data.WriteToPacket(ldfStream); } else { data->WriteToPacket(ldfStream); } @@ -782,7 +777,7 @@ void InventoryComponent::Update(float deltaTime) { } } -void InventoryComponent::UpdateSlot(const std::string& location, EquippedItem item, bool keepCurrent) { +void InventoryComponent::UpdateSlot(const std::string& location, const EquippedItem& item, bool keepCurrent) { const auto index = m_Equipped.find(location); if (index != m_Equipped.end()) { @@ -1080,7 +1075,7 @@ void InventoryComponent::PushEquippedItems() { } void InventoryComponent::PopEquippedItems() { - auto current = m_Equipped; + const auto current = m_Equipped; for (const auto& pair : current) { auto* const item = FindItemById(pair.second.id); @@ -1876,7 +1871,7 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo slot.PushDebug<AMFBoolValue>("Bind on equip") = item->GetInfo().isBOE; slot.PushDebug<AMFBoolValue>("Is currently bound") = item->GetBound(); auto& extra = slot.PushDebug("Extra Info"); - for (const auto* const setting : item->GetConfig()) { + for (const auto& setting : item->GetConfig().values | std::views::values) { if (setting) extra.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } } @@ -1892,7 +1887,7 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo equipSlot.PushDebug<AMFIntValue>("Slot") = info.slot; equipSlot.PushDebug<AMFIntValue>("Count") = info.count; auto& extra = equipSlot.PushDebug("Extra Info"); - for (const auto* const setting : info.config) { + for (const auto& setting : info.config.values | std::views::values) { if (setting) extra.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } } diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index beaf0efb..74ad326d 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -134,7 +134,7 @@ public: uint32_t count, eLootSourceType lootSourceType = eLootSourceType::NONE, eInventoryType inventoryType = INVALID, - const std::vector<LDFBaseData*>& config = {}, + const LwoNameValue& config = {}, LWOOBJID parent = LWOOBJID_EMPTY, bool showFlyingLoot = true, bool isModMoveAndEquip = false, @@ -213,7 +213,7 @@ public: * @param item the item to place * @param keepCurrent stores the item in an additional temp slot if there's already an item equipped */ - void UpdateSlot(const std::string& location, EquippedItem item, bool keepCurrent = false); + void UpdateSlot(const std::string& location, const EquippedItem& item, bool keepCurrent = false); /** * Removes a slot from the inventory diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 14093b15..48585237 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -221,8 +221,8 @@ void ModelComponent::RemoveBehavior(MoveToInventoryMessage& msg, const bool keep auto* const inventoryComponent = playerEntity->GetComponent<InventoryComponent>(); if (inventoryComponent && !behavior.GetIsLoot()) { // config is owned by the item - std::vector<LDFBaseData*> config; - config.push_back(new LDFData<std::string>(u"userModelName", behavior.GetName())); + LwoNameValue config; + config.Insert(u"userModelName", behavior.GetName()); inventoryComponent->AddItem(7965, 1, eLootSourceType::PROPERTY, eInventoryType::BEHAVIORS, config, LWOOBJID_EMPTY, true, false, msg.GetBehaviorId()); } } diff --git a/dGame/dComponents/PhantomPhysicsComponent.h b/dGame/dComponents/PhantomPhysicsComponent.h index 115783ac..9a9e1880 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.h +++ b/dGame/dComponents/PhantomPhysicsComponent.h @@ -12,7 +12,6 @@ #include "eReplicaComponentType.h" #include "PhysicsComponent.h" -class LDFBaseData; class Entity; class dpEntity; enum class ePhysicsEffectType : uint32_t ; diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 3c62dc52..54371fde 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -346,10 +346,7 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N info.spawner = nullptr; info.spawnerID = spawnerID; info.spawnerNodeID = 0; - - for (auto* setting : item->GetConfig()) { - info.settings.push_back(setting->Copy()); - } + info.settings = item->GetConfig(); Entity* newEntity = Game::entityManager->CreateEntity(info); if (newEntity != nullptr) { @@ -393,11 +390,11 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N auto* spawner = Game::zoneManager->GetSpawner(spawnerId); - info.nodes[0]->config.push_back(new LDFData<LWOOBJID>(u"modelBehaviors", 0)); - info.nodes[0]->config.push_back(new LDFData<LWOOBJID>(u"userModelID", info.spawnerID)); - info.nodes[0]->config.push_back(new LDFData<int>(u"modelType", 2)); - info.nodes[0]->config.push_back(new LDFData<bool>(u"propertyObjectID", true)); - info.nodes[0]->config.push_back(new LDFData<int>(u"componentWhitelist", 1)); + info.nodes[0]->config.Insert<LWOOBJID>(u"modelBehaviors", 0); + info.nodes[0]->config.Insert<LWOOBJID>(u"userModelID", info.spawnerID); + info.nodes[0]->config.Insert<int>(u"modelType", 2); + info.nodes[0]->config.Insert<bool>(u"propertyObjectID", true); + info.nodes[0]->config.Insert<int>(u"componentWhitelist", 1); auto* model = spawner->Spawn(); auto* modelComponent = model->GetComponent<ModelComponent>(); @@ -476,28 +473,19 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet if (model->GetLOT() == 14) { //add it to the inv - std::vector<LDFBaseData*> settings; - + LwoNameValue actualConfig; + //fill our settings with BBB gurbage - LDFBaseData* ldfBlueprintID = new LDFData<LWOOBJID>(u"blueprintid", model->GetVar<LWOOBJID>(u"blueprintid")); - LDFBaseData* userModelDesc = new LDFData<std::u16string>(u"userModelDesc", u"A cool model you made!"); - LDFBaseData* userModelHasBhvr = new LDFData<bool>(u"userModelHasBhvr", false); - LDFBaseData* userModelID = new LDFData<LWOOBJID>(u"userModelID", model->GetVar<LWOOBJID>(u"userModelID")); - LDFBaseData* userModelMod = new LDFData<bool>(u"userModelMod", false); - LDFBaseData* userModelName = new LDFData<std::u16string>(u"userModelName", u"My Cool Model"); - LDFBaseData* propertyObjectID = new LDFData<bool>(u"userModelOpt", true); - LDFBaseData* modelType = new LDFData<int>(u"userModelPhysicsType", 2); + actualConfig.Insert(u"blueprintid", model->GetVar<LWOOBJID>(u"blueprintid")); + actualConfig.Insert(u"userModelDesc", u"A cool model you made!"); + actualConfig.Insert(u"userModelHasBhvr", false); + actualConfig.Insert(u"userModelID", model->GetVar<LWOOBJID>(u"userModelID")); + actualConfig.Insert(u"userModelMod", false); + actualConfig.Insert(u"userModelName", u"My Cool Model"); + actualConfig.Insert(u"userModelOpt", true); + actualConfig.Insert(u"userModelPhysicsType", 2); - settings.push_back(ldfBlueprintID); - settings.push_back(userModelDesc); - settings.push_back(userModelHasBhvr); - settings.push_back(userModelID); - settings.push_back(userModelMod); - settings.push_back(userModelName); - settings.push_back(propertyObjectID); - settings.push_back(modelType); - - inventoryComponent->AddItem(6662, 1, eLootSourceType::DELETION, eInventoryType::MODELS_IN_BBB, settings, LWOOBJID_EMPTY, false, false, spawnerId); + inventoryComponent->AddItem(6662, 1, eLootSourceType::DELETION, eInventoryType::MODELS_IN_BBB, actualConfig, LWOOBJID_EMPTY, false, false, spawnerId); auto* item = inventoryComponent->FindItemBySubKey(spawnerId); if (item == nullptr) { @@ -615,23 +603,23 @@ void PropertyManagementComponent::Load() { info.spawnerID = databaseModel.id; - std::vector<LDFBaseData*> settings; + LwoNameValue& settings = node->config; //BBB property models need to have extra stuff set for them: if (databaseModel.lot == 14) { LWOOBJID blueprintID = databaseModel.ugcId; - settings.push_back(new LDFData<LWOOBJID>(u"blueprintid", blueprintID)); - settings.push_back(new LDFData<int>(u"componentWhitelist", 1)); - settings.push_back(new LDFData<int>(u"modelType", 2)); - settings.push_back(new LDFData<bool>(u"propertyObjectID", true)); - settings.push_back(new LDFData<LWOOBJID>(u"userModelID", databaseModel.id)); + settings.Insert<LWOOBJID>(u"blueprintid", blueprintID); + settings.Insert<int>(u"componentWhitelist", 1); + settings.Insert<int>(u"modelType", 2); + settings.Insert<bool>(u"propertyObjectID", true); + settings.Insert<LWOOBJID>(u"userModelID", databaseModel.id); } else { - settings.push_back(new LDFData<int>(u"modelType", 2)); - settings.push_back(new LDFData<LWOOBJID>(u"userModelID", databaseModel.id)); - settings.push_back(new LDFData<LWOOBJID>(u"modelBehaviors", 0)); - settings.push_back(new LDFData<bool>(u"propertyObjectID", true)); - settings.push_back(new LDFData<int>(u"componentWhitelist", 1)); + settings.Insert<int>(u"modelType", 2); + settings.Insert<LWOOBJID>(u"userModelID", databaseModel.id); + settings.Insert<LWOOBJID>(u"modelBehaviors", 0); + settings.Insert<bool>(u"propertyObjectID", true); + settings.Insert<int>(u"componentWhitelist", 1); } std::ostringstream userModelBehavior; @@ -646,9 +634,7 @@ void PropertyManagementComponent::Load() { firstAdded = true; } - settings.push_back(new LDFData<std::string>(u"userModelBehaviors", userModelBehavior.str())); - - node->config = settings; + settings.Insert<std::string>(u"userModelBehaviors", userModelBehavior.str()); const auto spawnerId = Game::zoneManager->MakeSpawner(info); diff --git a/dGame/dComponents/RacingControlComponent.cpp b/dGame/dComponents/RacingControlComponent.cpp index 06cd02be..9eb43b9e 100644 --- a/dGame/dComponents/RacingControlComponent.cpp +++ b/dGame/dComponents/RacingControlComponent.cpp @@ -26,6 +26,7 @@ #include "dZoneManager.h" #include "CDActivitiesTable.h" #include "eStateChangeType.h" +#include <ranges> #include <ctime> #ifndef M_PI @@ -57,6 +58,7 @@ RacingControlComponent::RacingControlComponent(Entity* parent, const int32_t com CDActivitiesTable* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>(); std::vector<CDActivities> activities = activitiesTable->Query([=](CDActivities entry) {return (entry.instanceMapID == worldID); }); for (CDActivities activity : activities) m_ActivityID = activity.ActivityID; + RegisterMsg(&RacingControlComponent::MsgConfigureRacingControl); } RacingControlComponent::~RacingControlComponent() {} @@ -178,11 +180,10 @@ void RacingControlComponent::LoadPlayerVehicle(Entity* player, moduleAssemblyComponent->SetSubKey(item->GetSubKey()); moduleAssemblyComponent->SetUseOptionalParts(false); - for (auto* config : item->GetConfig()) { - if (config->GetKey() == u"assemblyPartLOTs") { - moduleAssemblyComponent->SetAssemblyPartsLOTs( - GeneralUtils::ASCIIToUTF16(config->GetValueAsString())); - } + const auto& lnv = item->GetConfig().values; + const auto itr = lnv.find(u"assemblyPartLOTs"); + if (itr != lnv.end()) { + moduleAssemblyComponent->SetAssemblyPartsLOTs(GeneralUtils::ASCIIToUTF16(itr->second->GetValueAsString())); } } @@ -871,10 +872,10 @@ void RacingControlComponent::Update(float deltaTime) { } } -void RacingControlComponent::MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg) { - for (const auto& dataUnique : msg.racingSettings) { +bool RacingControlComponent::MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg) { + for (const auto& dataUnique : msg.racingSettings | std::views::values) { if (!dataUnique) continue; - const auto* const data = dataUnique.get(); + const auto* const data = dataUnique.get(); if (data->GetKey() == u"Race_PathName" && data->GetValueType() == LDF_TYPE_UTF_16) { m_PathName = static_cast<const LDFData<std::u16string>*>(data)->GetValue(); } else if (data->GetKey() == u"activityID" && data->GetValueType() == LDF_TYPE_S32) { @@ -886,4 +887,5 @@ void RacingControlComponent::MsgConfigureRacingControl(const GameMessages::Confi m_MinimumPlayersForGroupAchievements = static_cast<const LDFData<int32_t>*>(data)->GetValue(); } } + return true; } diff --git a/dGame/dComponents/RacingControlComponent.h b/dGame/dComponents/RacingControlComponent.h index b4300d94..bedb636b 100644 --- a/dGame/dComponents/RacingControlComponent.h +++ b/dGame/dComponents/RacingControlComponent.h @@ -152,7 +152,7 @@ public: */ RacingPlayerInfo* GetPlayerData(LWOOBJID playerID); - void MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg); + bool MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg); private: diff --git a/dGame/dComponents/ScriptComponent.cpp b/dGame/dComponents/ScriptComponent.cpp index 87b6e6e1..2c9be34b 100644 --- a/dGame/dComponents/ScriptComponent.cpp +++ b/dGame/dComponents/ScriptComponent.cpp @@ -8,6 +8,8 @@ #include "GameMessages.h" #include "Amf3.h" +#include <ranges> + ScriptComponent::ScriptComponent(Entity* parent, const int32_t componentID, const std::string& scriptName, bool serialized, bool client) : Component(parent, componentID) { m_Serialized = serialized; m_Client = client; @@ -20,7 +22,7 @@ ScriptComponent::ScriptComponent(Entity* parent, const int32_t componentID, cons void ScriptComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { if (bIsInitialUpdate) { const auto& networkSettings = m_Parent->GetNetworkSettings(); - auto hasNetworkSettings = !networkSettings.empty(); + auto hasNetworkSettings = !networkSettings.values.empty(); outBitStream.Write(hasNetworkSettings); if (hasNetworkSettings) { @@ -28,9 +30,9 @@ void ScriptComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitial // First write the most inner LDF data RakNet::BitStream ldfData; ldfData.Write<uint8_t>(0); - ldfData.Write<uint32_t>(networkSettings.size()); + ldfData.Write<uint32_t>(networkSettings.values.size()); - for (auto* networkSetting : networkSettings) { + for (const auto& networkSetting : networkSettings.values | std::views::values) { networkSetting->WriteToPacket(ldfData); } @@ -56,7 +58,7 @@ bool ScriptComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& r auto& scriptInfo = reportInfo.info->PushDebug("Script"); scriptInfo.PushDebug<AMFStringValue>("Script Name") = m_ScriptName.empty() ? "None" : m_ScriptName; auto& networkSettings = scriptInfo.PushDebug("Network Settings"); - for (const auto* const setting : m_Parent->GetNetworkSettings()) { + for (const auto& setting : m_Parent->GetNetworkSettings().values | std::views::values) { networkSettings.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index 5ac27bb6..1d4eb29d 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -125,8 +125,7 @@ void SkillComponent::SyncPlayerProjectile(const LWOOBJID projectileId, RakNet::B this->m_managedProjectiles.erase(this->m_managedProjectiles.begin() + index); GameMessages::ActivityNotify notify; - notify.notification.push_back( std::make_unique<LDFData<int32_t>>(u"shot_done", sync_entry.skillId)); - + notify.notification.Insert<int32_t>(u"shot_done", sync_entry.skillId); m_Parent->OnActivityNotify(notify); } diff --git a/dGame/dEntity/EntityInfo.h b/dGame/dEntity/EntityInfo.h index e16c315d..68eaa95d 100644 --- a/dGame/dEntity/EntityInfo.h +++ b/dGame/dEntity/EntityInfo.h @@ -34,7 +34,7 @@ struct EntityInfo { LOT lot; NiPoint3 pos; NiQuaternion rot = QuatUtils::IDENTITY; - std::vector<LDFBaseData*> settings; - std::vector<LDFBaseData*> networkSettings; + LwoNameValue settings; + LwoNameValue networkSettings; float scale; }; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 5edd47f2..6b57e177 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -46,6 +46,7 @@ #include <sstream> #include <future> #include <chrono> +#include <ranges> #include "RakString.h" //CDB includes: @@ -465,20 +466,20 @@ void GameMessages::SendAddItemToInventoryClientSync(Entity* entity, const System bitStream.Write(lootSourceType != eLootSourceType::NONE); // Loot source if (lootSourceType != eLootSourceType::NONE) bitStream.Write(lootSourceType); - LWONameValue extraInfo; + std::u16string extraInfo; - auto config = item->GetConfig(); + const auto& config = item->GetConfig(); - for (auto* data : config) { - extraInfo.name += GeneralUtils::ASCIIToUTF16(data->GetString()) + u","; + for (const auto& data : config.values | std::views::values) { + extraInfo += GeneralUtils::ASCIIToUTF16(data->GetString()) + u","; } - if (extraInfo.name.length() > 0) extraInfo.name.pop_back(); // remove the last comma + if (extraInfo.length() > 0) extraInfo.pop_back(); // remove the last comma - bitStream.Write<uint32_t>(extraInfo.name.size()); - if (extraInfo.name.size() > 0) { - for (uint32_t i = 0; i < extraInfo.name.size(); ++i) { - bitStream.Write<uint16_t>(extraInfo.name[i]); + bitStream.Write<uint32_t>(extraInfo.size()); + if (extraInfo.size() > 0) { + for (uint32_t i = 0; i < extraInfo.size(); ++i) { + bitStream.Write<uint16_t>(extraInfo[i]); } bitStream.Write<uint16_t>(0x00); } @@ -743,13 +744,9 @@ void GameMessages::SendBroadcastTextToChatbox(Entity* entity, const SystemAddres bitStream.Write(entity->GetObjectID()); bitStream.Write(MessageType::Game::BROADCAST_TEXT_TO_CHATBOX); - LWONameValue attribs; - attribs.name = attrs; - attribs.length = attrs.size(); - - bitStream.Write<uint32_t>(attribs.length); - for (uint32_t i = 0; i < attribs.length; ++i) { - bitStream.Write<uint16_t>(attribs.name[i]); + bitStream.Write<uint32_t>(attrs.size()); + for (uint32_t i = 0; i < attrs.size(); ++i) { + bitStream.Write<uint16_t>(attrs[i]); } bitStream.Write<uint16_t>(0x00); // Null Terminator @@ -2621,11 +2618,11 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent info.spawnerID = entity->GetObjectID(); info.spawnerNodeID = 0; - info.settings.push_back(new LDFData<LWOOBJID>(u"blueprintid", blueprintIDs[i])); - info.settings.push_back(new LDFData<int>(u"componentWhitelist", 1)); - info.settings.push_back(new LDFData<int>(u"modelType", 2)); - info.settings.push_back(new LDFData<bool>(u"propertyObjectID", true)); - info.settings.push_back(new LDFData<LWOOBJID>(u"userModelID", modelIDs[i])); + info.settings.Insert<LWOOBJID>(u"blueprintid", blueprintIDs[i]); + info.settings.Insert<int>(u"componentWhitelist", 1); + info.settings.Insert<int>(u"modelType", 2); + info.settings.Insert<bool>(u"propertyObjectID", true); + info.settings.Insert<LWOOBJID>(u"userModelID", modelIDs[i]); Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); if (newEntity) { Game::entityManager->ConstructEntity(newEntity); @@ -5266,7 +5263,8 @@ void GameMessages::HandleRemoveItemFromInventory(RakNet::BitStream& inStream, En int eInvType = INVENTORY_MAX; bool eLootTypeSourceIsDefault = false; int eLootTypeSource = LOOTTYPE_NONE; - LWONameValue extraInfo; + int32_t extraInfoLength = 0; + std::u16string extraInfo; bool forceDeletion = true; bool iLootTypeSourceIsDefault = false; LWOOBJID iLootTypeSource = LWOOBJID_EMPTY; @@ -5292,12 +5290,12 @@ void GameMessages::HandleRemoveItemFromInventory(RakNet::BitStream& inStream, En if (eInvTypeIsDefault) inStream.Read(eInvType); inStream.Read(eLootTypeSourceIsDefault); if (eLootTypeSourceIsDefault) inStream.Read(eLootTypeSource); - inStream.Read(extraInfo.length); - if (extraInfo.length > 0) { - for (uint32_t i = 0; i < extraInfo.length; ++i) { + inStream.Read(extraInfoLength); + if (extraInfoLength > 0) { + for (uint32_t i = 0; i < extraInfoLength; ++i) { uint16_t character; inStream.Read(character); - extraInfo.name.push_back(character); + extraInfo.push_back(character); } uint16_t nullTerm; inStream.Read(nullTerm); @@ -5505,10 +5503,8 @@ void GameMessages::HandleModularBuildFinish(RakNet::BitStream& inStream, Entity* //inv->UnequipItem(inv->GetItemStackByLOT(6086, eInventoryType::ITEMS)); // take off the thinking cap //Game::entityManager->SerializeEntity(entity); - const auto moduleAssembly = new LDFData<std::u16string>(u"assemblyPartLOTs", modules); - - std::vector<LDFBaseData*> config; - config.push_back(moduleAssembly); + LwoNameValue config; + config.Insert(u"assemblyPartLOTs", modules); LWOOBJID newID = ObjectIDManager::GetPersistentID(); @@ -5754,7 +5750,6 @@ void GameMessages::HandleUseNonEquipmentItem(RakNet::BitStream& inStream, Entity void GameMessages::HandleMatchRequest(RakNet::BitStream& inStream, Entity* entity) { LWOOBJID activator; - //std::map<LWOOBJID, LWONameValue> additionalPlayers; uint32_t playerChoicesLen; std::string playerChoices; int type; @@ -6309,7 +6304,7 @@ namespace GameMessages { bitStream.Write(id); std::string toWrite; - for (const auto* item : localizeParams) { + for (const auto& item : localizeParams | std::views::values) { toWrite += item->GetString() + "\n"; } if (!toWrite.empty()) toWrite.pop_back(); diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index b7558d4b..8937e028 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -13,6 +13,7 @@ #include "Brick.h" #include "MessageType/Game.h" #include "eGameMasterLevel.h" +#include "LDFFormat.h" class AMFBaseValue; class AMFArrayValue; @@ -711,7 +712,7 @@ namespace GameMessages { bool translate{}; int32_t time{}; std::u16string id{}; - std::vector<LDFBaseData*> localizeParams{}; + LwoNameValue localizeParams{}; std::u16string imageName{}; std::u16string text{}; void Serialize(RakNet::BitStream& bitStream) const override; @@ -734,7 +735,7 @@ namespace GameMessages { struct ConfigureRacingControl : public GameMsg { ConfigureRacingControl() : GameMsg(MessageType::Game::CONFIGURE_RACING_CONTROL) {} - std::vector<std::unique_ptr<LDFBaseData>> racingSettings{}; + LwoNameValue racingSettings{}; }; struct SetModelToBuild : public GameMsg { @@ -754,7 +755,7 @@ namespace GameMessages { struct ActivityNotify : public GameMsg { ActivityNotify() : GameMsg(MessageType::Game::ACTIVITY_NOTIFY) {} - std::vector<std::unique_ptr<LDFBaseData>> notification{}; + LwoNameValue notification{}; }; struct ShootingGalleryFire : public GameMsg { diff --git a/dGame/dInventory/EquippedItem.h b/dGame/dInventory/EquippedItem.h index 78da9e8d..ec10f95b 100644 --- a/dGame/dInventory/EquippedItem.h +++ b/dGame/dInventory/EquippedItem.h @@ -31,5 +31,5 @@ struct EquippedItem /** * The configuration of the item with any extra data */ - std::vector<LDFBaseData*> config = {}; + LwoNameValue config = {}; }; diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 19b536a4..f18cd0b3 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -28,6 +28,7 @@ #include "CDObjectSkillsTable.h" #include "CDComponentsRegistryTable.h" #include "CDPackageComponentTable.h" +#include <ranges> namespace { const std::map<std::string, std::string> ExtraSettingAbbreviations = { @@ -46,7 +47,7 @@ namespace { }; } -Item::Item(const LWOOBJID id, const LOT lot, Inventory* inventory, const uint32_t slot, const uint32_t count, const bool bound, const std::vector<LDFBaseData*>& config, const LWOOBJID parent, LWOOBJID subKey, eLootSourceType lootSourceType) { +Item::Item(const LWOOBJID id, const LOT lot, Inventory* inventory, const uint32_t slot, const uint32_t count, const bool bound, const LwoNameValue& config, const LWOOBJID parent, LWOOBJID subKey, eLootSourceType lootSourceType) { if (!Inventory::IsValidItem(lot)) { return; } @@ -71,7 +72,7 @@ Item::Item( Inventory* inventory, const uint32_t slot, const uint32_t count, - const std::vector<LDFBaseData*>& config, + const LwoNameValue& config, const LWOOBJID parent, bool showFlyingLoot, bool isModMoveAndEquip, @@ -131,11 +132,11 @@ uint32_t Item::GetSlot() const { return slot; } -std::vector<LDFBaseData*> Item::GetConfig() const { +const LwoNameValue& Item::GetConfig() const { return config; } -std::vector<LDFBaseData*>& Item::GetConfig() { +LwoNameValue& Item::GetConfig() { return config; } @@ -379,7 +380,7 @@ void Item::UseNonEquip(Item* item) { } void Item::Disassemble(const eInventoryType inventoryType) { - for (auto* data : config) { + for (const auto& data : config.values | std::views::values) { if (data->GetKey() == u"assemblyPartLOTs") { auto modStr = data->GetValueAsString(); @@ -530,18 +531,12 @@ void Item::RemoveFromInventory() { Item::~Item() { delete preconditions; - - for (auto* value : config) { - delete value; - } - - config.clear(); } void Item::SaveConfigXml(tinyxml2::XMLElement& i) const { tinyxml2::XMLElement* x = nullptr; - for (const auto* config : this->config) { + for (const auto& config : config.values | std::views::values) { const auto& key = GeneralUtils::UTF16ToWTF8(config->GetKey()); const auto saveKey = ExtraSettingAbbreviations.find(key); if (saveKey == ExtraSettingAbbreviations.end()) { @@ -561,12 +556,11 @@ void Item::LoadConfigXml(const tinyxml2::XMLElement& i) { const auto* x = i.FirstChildElement("x"); if (!x) return; - for (const auto& pair : ExtraSettingAbbreviations) { - const auto* data = x->Attribute(pair.second.c_str()); + for (const auto& [fullName, abbreviation] : ExtraSettingAbbreviations) { + const auto* data = x->Attribute(abbreviation.c_str()); if (!data) continue; - const auto value = pair.first + "=" + data; - config.push_back(LDFBaseData::DataFromString(value)); + config.ParseInsert(fullName + "=" + data); } } diff --git a/dGame/dInventory/Item.h b/dGame/dInventory/Item.h index 846a7aa7..baccf28f 100644 --- a/dGame/dInventory/Item.h +++ b/dGame/dInventory/Item.h @@ -40,7 +40,7 @@ public: uint32_t slot, uint32_t count, bool bound, - const std::vector<LDFBaseData*>& config, + const LwoNameValue& config, LWOOBJID parent, LWOOBJID subKey, eLootSourceType lootSourceType = eLootSourceType::NONE @@ -64,7 +64,7 @@ public: Inventory* inventory, uint32_t slot = 0, uint32_t count = 1, - const std::vector<LDFBaseData*>& config = {}, + const LwoNameValue& config = {}, LWOOBJID parent = LWOOBJID_EMPTY, bool showFlyingLoot = true, bool isModMoveAndEquip = false, @@ -118,13 +118,13 @@ public: * Returns current config info for this item, e.g. for rockets * @return current config info for this item */ - std::vector<LDFBaseData*>& GetConfig(); + LwoNameValue& GetConfig(); /** * Returns current config info for this item, e.g. for rockets * @return current config info for this item */ - std::vector<LDFBaseData*> GetConfig() const; + const LwoNameValue& GetConfig() const; /** * Returns the database info for this item @@ -269,7 +269,7 @@ private: /** * Config data for this item, e.g. for rocket parts and car parts */ - std::vector<LDFBaseData*> config; + LwoNameValue config; /** * The inventory this item belongs to diff --git a/dGame/dMission/MissionTask.cpp b/dGame/dMission/MissionTask.cpp index 872796a8..f23deb6e 100644 --- a/dGame/dMission/MissionTask.cpp +++ b/dGame/dMission/MissionTask.cpp @@ -218,7 +218,6 @@ void MissionTask::Progress(int32_t value, LWOOBJID associate, const std::string& uint32_t activityId; uint32_t lot; uint32_t collectionId; - std::vector<LDFBaseData*> settings; switch (type) { case eMissionTaskType::UNKNOWN: diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index f5dbd29f..745798b8 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -802,7 +802,7 @@ namespace DEVGMCommands { info.spawner = nullptr; info.spawnerID = entity->GetObjectID(); info.spawnerNodeID = 0; - info.settings = { new LDFData<bool>(u"SpawnedFromSlashCommand", true) }; + info.settings.Insert<bool>(u"SpawnedFromSlashCommand", true); Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); @@ -844,7 +844,7 @@ namespace DEVGMCommands { info.spawner = nullptr; info.spawnerID = entity->GetObjectID(); info.spawnerNodeID = 0; - info.settings = { new LDFData<bool>(u"SpawnedFromSlashCommand", true) }; + info.settings.Insert(u"SpawnedFromSlashCommand", true); auto playerPosition = entity->GetPosition(); while (numberToSpawn > 0) { @@ -1268,10 +1268,10 @@ namespace DEVGMCommands { auto* inventoryComponent = entity->GetComponent<InventoryComponent>(); if (!inventoryComponent) return; - std::vector<LDFBaseData*> data{}; - data.push_back(new LDFData<int32_t>(u"reforgedLOT", reforgedItem.value())); + LwoNameValue config; + config.Insert<LOT>(u"reforgedLOT", reforgedItem.value()); - inventoryComponent->AddItem(baseItem.value(), 1, eLootSourceType::MODERATION, eInventoryType::INVALID, data); + inventoryComponent->AddItem(baseItem.value(), 1, eLootSourceType::MODERATION, eInventoryType::INVALID, config); } void Crash(Entity* entity, const SystemAddress& sysAddr, const std::string args) { diff --git a/dGame/dUtilities/VanityUtilities.cpp b/dGame/dUtilities/VanityUtilities.cpp index 48d3c0da..950f26e2 100644 --- a/dGame/dUtilities/VanityUtilities.cpp +++ b/dGame/dUtilities/VanityUtilities.cpp @@ -45,10 +45,8 @@ void VanityUtilities::SpawnVanity() { info.pos = { 259.5f, 246.4f, -705.2f }; info.rot = { 0.0f, 0.0f, 1.0f, 0.0f }; info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID(); - info.settings = { - new LDFData<bool>(u"hasCustomText", true), - new LDFData<std::string>(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string())) - }; + info.settings.Insert<bool>(u"hasCustomText", true); + info.settings.Insert<std::string>(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string())); auto* entity = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(entity); @@ -231,7 +229,7 @@ void ParseXml(const std::string& file) { auto* configElement = object->FirstChildElement("config"); std::vector<std::u16string> keys = {}; - std::vector<LDFBaseData*> config = {}; + LwoNameValue config; if (configElement) { for (auto* key = configElement->FirstChildElement("key"); key != nullptr; key = key->NextSiblingElement("key")) { @@ -239,16 +237,16 @@ void ParseXml(const std::string& file) { auto* data = key->GetText(); if (!data) continue; - LDFBaseData* configData = LDFBaseData::DataFromString(data); + const auto& configData = config.ParseInsert(data); if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN) { - useLocationsAsRandomSpawnPoint = static_cast<bool>(configData); + useLocationsAsRandomSpawnPoint = static_cast<const LDFData<bool>*>(configData.get())->GetValue(); + config.Erase(u"useLocationsAsRandomSpawnPoint"); continue; } keys.push_back(configData->GetKey()); - config.push_back(configData); } } - if (!keys.empty()) config.push_back(new LDFData<std::vector<std::u16string>>(u"syncLDF", keys)); + if (!keys.empty()) config.Insert<std::vector<std::u16string>>(u"syncLDF", keys); VanityObject objectData{ .m_Name = name, diff --git a/dGame/dUtilities/VanityUtilities.h b/dGame/dUtilities/VanityUtilities.h index 2afd4a6b..4da74479 100644 --- a/dGame/dUtilities/VanityUtilities.h +++ b/dGame/dUtilities/VanityUtilities.h @@ -19,7 +19,7 @@ struct VanityObject { std::vector<LOT> m_Equipment; std::vector<std::string> m_Phrases; std::map<uint32_t, std::vector<VanityObjectLocation>> m_Locations; - std::vector<LDFBaseData*> m_Config; + LwoNameValue m_Config; }; diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index 91117056..e963b10c 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -11,6 +11,7 @@ #include "BitStreamUtils.h" #include <iostream> +#include <ranges> void HTTPMonitorInfo::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(port); @@ -88,18 +89,18 @@ void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t rep RakNet::BitStream data; - std::vector<std::unique_ptr<LDFBaseData>> ldfData; - ldfData.push_back(std::move(make_unique<LDFData<LWOOBJID>>(u"objid", player))); - ldfData.push_back(std::move(make_unique<LDFData<LOT>>(u"template", 1))); - ldfData.push_back(std::move(make_unique<LDFData<string>>(u"xmlData", xmlData))); - ldfData.push_back(std::move(make_unique<LDFData<u16string>>(u"name", username))); - ldfData.push_back(std::move(make_unique<LDFData<int32_t>>(u"gmlevel", static_cast<int32_t>(gm)))); - ldfData.push_back(std::move(make_unique<LDFData<int32_t>>(u"chatmode", static_cast<int32_t>(gm)))); - ldfData.push_back(std::move(make_unique<LDFData<int64_t>>(u"reputation", reputation))); - ldfData.push_back(std::move(make_unique<LDFData<int32_t>>(u"propertycloneid", cloneID))); + LwoNameValue ldfData; + ldfData.Insert<LWOOBJID>(u"objid", player); + ldfData.Insert<LOT>(u"template", 1); + ldfData.Insert<string>(u"xmlData", xmlData); + ldfData.Insert<u16string>(u"name", username); + ldfData.Insert<int32_t>(u"gmlevel", static_cast<int32_t>(gm)); + ldfData.Insert<int32_t>(u"chatmode", static_cast<int32_t>(gm)); + ldfData.Insert<int64_t>(u"reputation", reputation); + ldfData.Insert<int32_t>(u"propertycloneid", cloneID); - data.Write<uint32_t>(ldfData.size()); - for (const auto& toSerialize : ldfData) toSerialize->WriteToPacket(data); + data.Write<uint32_t>(ldfData.values.size()); + for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data); //Compress the data before sending: const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed()); diff --git a/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp b/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp index 61d25436..5fbbda20 100644 --- a/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp +++ b/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp @@ -97,17 +97,14 @@ void AmDarklingDragon::OnHitOrHealResult(Entity* self, Entity* attacker, int32_t info.pos = objectPosition; info.rot = rotation; info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData<std::string>(u"rebuild_activators", + info.settings.Insert<std::string>(u"rebuild_activators", std::to_string(objectPosition.x + forward.x) + "\x1f" + std::to_string(objectPosition.y) + "\x1f" + - std::to_string(objectPosition.z + forward.z) - ), - new LDFData<int32_t>(u"respawn", 100000), - new LDFData<float>(u"rebuild_reset_time", 15), - new LDFData<bool>(u"no_timed_spawn", true), - new LDFData<LWOOBJID>(u"Dragon", self->GetObjectID()) - }; + std::to_string(objectPosition.z + forward.z)); + info.settings.Insert<int32_t>(u"respawn", 100000); + info.settings.Insert<float>(u"rebuild_reset_time", 15); + info.settings.Insert<bool>(u"no_timed_spawn", true); + info.settings.Insert<LWOOBJID>(u"Dragon", self->GetObjectID()); auto* golemObject = Game::entityManager->CreateEntity(info); diff --git a/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp b/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp index b4b038b8..2ddc6309 100644 --- a/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp +++ b/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp @@ -113,17 +113,14 @@ void FvMaelstromDragon::OnHitOrHealResult(Entity* self, Entity* attacker, int32_ info.pos = objectPosition; info.rot = rotation; info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData<std::string>(u"rebuild_activators", + info.settings.Insert<std::string>(u"rebuild_activators", std::to_string(objectPosition.x + forward.x) + "\x1f" + std::to_string(objectPosition.y) + "\x1f" + - std::to_string(objectPosition.z + forward.z) - ), - new LDFData<int32_t>(u"respawn", 100000), - new LDFData<float>(u"rebuild_reset_time", 15), - new LDFData<bool>(u"no_timed_spawn", true), - new LDFData<LWOOBJID>(u"Dragon", self->GetObjectID()) - }; + std::to_string(objectPosition.z + forward.z)); + info.settings.Insert<int32_t>(u"respawn", 100000); + info.settings.Insert<float>(u"rebuild_reset_time", 15); + info.settings.Insert<bool>(u"no_timed_spawn", true); + info.settings.Insert<LWOOBJID>(u"Dragon", self->GetObjectID()); auto* golemObject = Game::entityManager->CreateEntity(info); diff --git a/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp b/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp index 9c71c284..26948c57 100644 --- a/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp +++ b/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp @@ -82,15 +82,12 @@ void BaseEnemyApe::OnTimerDone(Entity* self, std::string timerName) { entityInfo.spawnerID = self->GetObjectID(); entityInfo.lot = self->GetVar<LOT>(u"QuickbuildAnchorLOT") != 0 ? self->GetVar<LOT>(u"QuickbuildAnchorLOT") : 7549; - entityInfo.settings = { - new LDFData<std::string>(u"rebuild_activators", + entityInfo.settings.Insert<std::string>(u"rebuild_activators", std::to_string(objectPosition.GetX()) + "\x1f" + std::to_string(objectPosition.GetY()) + "\x1f" + - std::to_string(objectPosition.GetZ()) - ), - new LDFData<bool>(u"no_timed_spawn", true), - new LDFData<LWOOBJID>(u"ape", self->GetObjectID()) - }; + std::to_string(objectPosition.GetZ())); + entityInfo.settings.Insert<bool>(u"no_timed_spawn", true); + entityInfo.settings.Insert<LWOOBJID>(u"ape", self->GetObjectID()); auto* anchor = Game::entityManager->CreateEntity(entityInfo, nullptr, self); Game::entityManager->ConstructEntity(anchor); diff --git a/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp b/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp index 8e70d4c3..45f1e9ad 100644 --- a/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp +++ b/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp @@ -23,7 +23,7 @@ void BaseEnemyMech::OnDie(Entity* self, Entity* killer) { NiPoint3 newLoc = { controlPhys->GetPosition().x, dpWorld::GetNavMesh()->GetHeightAtPoint(controlPhys->GetPosition()), controlPhys->GetPosition().z }; EntityInfo info = EntityInfo(); - std::vector<LDFBaseData*> cfg; + LwoNameValue cfg; std::u16string activatorPosStr; activatorPosStr += (GeneralUtils::to_u16string(controlPhys->GetPosition().x)); activatorPosStr.push_back(0x1f); @@ -31,8 +31,7 @@ void BaseEnemyMech::OnDie(Entity* self, Entity* killer) { activatorPosStr.push_back(0x1f); activatorPosStr += (GeneralUtils::to_u16string(controlPhys->GetPosition().z)); - LDFBaseData* activatorPos = new LDFData<std::u16string>(u"rebuild_activators", activatorPosStr); - cfg.push_back(activatorPos); + cfg.Insert<std::u16string>(u"rebuild_activators", activatorPosStr); info.lot = qbTurretLOT; info.pos = newLoc; info.rot = controlPhys->GetRotation(); diff --git a/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp b/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp index 2091041b..2b367ed2 100644 --- a/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp +++ b/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp @@ -71,9 +71,7 @@ void ZoneAgSpiderQueen::OnTimerDone(Entity* self, std::string timerName) { info.pos = spawnTarget->GetPosition(); info.rot = spawnTarget->GetRotation(); info.lot = chestObject; - info.settings = { - new LDFData<LWOOBJID>(u"parent_tag", self->GetObjectID()) - }; + info.settings.Insert(u"parent_tag", self->GetObjectID()); auto* chest = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(chest); diff --git a/dScripts/02_server/Map/AM/AmSkullkinTower.cpp b/dScripts/02_server/Map/AM/AmSkullkinTower.cpp index 17ae2ea9..19c39f8b 100644 --- a/dScripts/02_server/Map/AM/AmSkullkinTower.cpp +++ b/dScripts/02_server/Map/AM/AmSkullkinTower.cpp @@ -37,12 +37,10 @@ void AmSkullkinTower::SpawnLegs(Entity* self, const std::string& loc) { return; } - std::vector<LDFBaseData*> config = { new LDFData<std::string>(u"Leg", loc) }; - EntityInfo info{}; info.lot = legLOT; info.spawnerID = self->GetObjectID(); - info.settings = config; + info.settings.Insert("Leg", loc); info.rot = newRot; if (loc == "Right") { diff --git a/dScripts/02_server/Map/General/PetDigServer.cpp b/dScripts/02_server/Map/General/PetDigServer.cpp index 77a50e5a..e20194a4 100644 --- a/dScripts/02_server/Map/General/PetDigServer.cpp +++ b/dScripts/02_server/Map/General/PetDigServer.cpp @@ -204,12 +204,10 @@ void PetDigServer::SpawnPet(Entity* self, const Entity* owner, const DigInfo dig info.pos = self->GetPosition(); info.rot = self->GetRotation(); info.spawnerID = self->GetSpawnerID(); - info.settings = { - new LDFData<LWOOBJID>(u"tamer", owner->GetObjectID()), - new LDFData<std::string>(u"group", "pet" + std::to_string(owner->GetObjectID())), - new LDFData<std::string>(u"spawnAnim", "spawn-pet"), - new LDFData<float>(u"spawnTimer", 1.0) - }; + info.settings.Insert<LWOOBJID>(u"tamer", owner->GetObjectID()); + info.settings.Insert<std::string>(u"group", "pet" + std::to_string(owner->GetObjectID())); + info.settings.Insert<std::string>(u"spawnAnim", "spawn-pet"); + info.settings.Insert<float>(u"spawnTimer", 1.0); auto* spawnedPet = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(spawnedPet); diff --git a/dScripts/02_server/Map/General/QbSpawner.cpp b/dScripts/02_server/Map/General/QbSpawner.cpp index 0562fdd8..9081ec57 100644 --- a/dScripts/02_server/Map/General/QbSpawner.cpp +++ b/dScripts/02_server/Map/General/QbSpawner.cpp @@ -66,14 +66,12 @@ void QbSpawner::OnTimerDone(Entity* self, std::string timerName) { info.rot = newRot; info.spawnerID = self->GetObjectID(); info.spawnerNodeID = 0; - info.settings = { - new LDFData<bool>(u"no_timed_spawn", true), - new LDFData<float>(u"aggroRadius", 70), - new LDFData<float>(u"softtetherRadius", 80), - new LDFData<float>(u"tetherRadius", 90), - new LDFData<float>(u"wanderRadius", 5), - new LDFData<int>(u"mobTableLoc", i) - }; + info.settings.Insert<bool>(u"no_timed_spawn", true); + info.settings.Insert<float>(u"aggroRadius", 70); + info.settings.Insert<float>(u"softtetherRadius", 80); + info.settings.Insert<float>(u"tetherRadius", 90); + info.settings.Insert<float>(u"wanderRadius", 5); + info.settings.Insert<int>(u"mobTableLoc", i); auto* child = Game::entityManager->CreateEntity(info, nullptr, self); Game::entityManager->ConstructEntity(child); diff --git a/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp b/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp index 5306a1dd..aab66449 100644 --- a/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp +++ b/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp @@ -34,12 +34,10 @@ void NsConcertChoiceBuildManager::SpawnCrate(Entity* self) { info.pos = self->GetPosition(); info.rot = self->GetRotation(); info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData<bool>(u"startsQBActivator", true), - new LDFData<std::string>(u"grpNameQBShowBricks", crate.group + std::to_string(groupNumber)), - new LDFData<std::u16string>(u"groupID", GeneralUtils::ASCIIToUTF16("Crate_" + group)), - new LDFData<float>(u"crateTime", crate.time), - }; + info.settings.Insert<bool>(u"startsQBActivator", true); + info.settings.Insert<std::string>(u"grpNameQBShowBricks", crate.group + std::to_string(groupNumber)); + info.settings.Insert<std::u16string>(u"groupID", GeneralUtils::ASCIIToUTF16("Crate_" + group)); + info.settings.Insert<float>(u"crateTime", crate.time); auto* spawnedCrate = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(spawnedCrate); diff --git a/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp b/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp index 2b2f16f9..c036b023 100644 --- a/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp +++ b/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp @@ -89,7 +89,7 @@ void NtCombatChallengeServer::SpawnTargetDummy(Entity* self) { info.spawnerID = self->GetObjectID(); info.pos = self->GetPosition(); info.rot = self->GetRotation(); - info.settings = { new LDFData<std::string>(u"custom_script_server", "scripts\\02_server\\Map\\NT\\L_NT_COMBAT_CHALLENGE_DUMMY.lua") }; + info.settings.Insert(u"custom_script_server", "scripts\\02_server\\Map\\NT\\L_NT_COMBAT_CHALLENGE_DUMMY.lua"); auto* dummy = Game::entityManager->CreateEntity(info, nullptr, self); diff --git a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp index 39fae3aa..18054365 100644 --- a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp +++ b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp @@ -95,10 +95,8 @@ void ZoneAgProperty::LoadInstance(Entity* self) { for (auto* spawner : Game::zoneManager->GetSpawnersByName(self->GetVar<std::string>(InstancerSpawner))) { for (auto* spawnerNode : spawner->m_Info.nodes) { - spawnerNode->config.push_back( - new LDFData<std::string>(u"custom_script_server", - R"(scripts\ai\GENERAL\L_INSTANCE_EXIT_TRANSFER_PLAYER_TO_LAST_NON_INSTANCE.lua)")); - spawnerNode->config.push_back(new LDFData<std::u16string>(u"transferText", u"SPIDER_QUEEN_EXIT_QUESTION")); + spawnerNode->config.Insert<std::string>(u"custom_script_server", R"(scripts\ai\GENERAL\L_INSTANCE_EXIT_TRANSFER_PLAYER_TO_LAST_NON_INSTANCE.lua)"); + spawnerNode->config.Insert<std::u16string>(u"transferText", u"SPIDER_QUEEN_EXIT_QUESTION"); } } diff --git a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp index d7be9d32..be47b53b 100644 --- a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp +++ b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp @@ -515,9 +515,7 @@ void NjMonastryBossInstance::FightOver(Entity* self) { info.pos = treasureChest->GetPosition(); info.rot = treasureChest->GetRotation(); info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData<LWOOBJID>(u"parent_tag", self->GetObjectID()) - }; + info.settings.Insert<LWOOBJID>(u"parent_tag", self->GetObjectID()); // Finally spawn a treasure chest at the correct spawn point auto* chestObject = Game::entityManager->CreateEntity(info); diff --git a/dScripts/02_server/Objects/StinkyFishTarget.cpp b/dScripts/02_server/Objects/StinkyFishTarget.cpp index 9840235d..ae595b89 100644 --- a/dScripts/02_server/Objects/StinkyFishTarget.cpp +++ b/dScripts/02_server/Objects/StinkyFishTarget.cpp @@ -21,9 +21,7 @@ void StinkyFishTarget::OnSkillEventFired(Entity* self, Entity* caster, const std entityInfo.pos = self->GetPosition(); entityInfo.rot = self->GetRotation(); entityInfo.spawnerID = self->GetObjectID(); - entityInfo.settings = { - new LDFData<bool>(u"no_timed_spawn", true) - }; + entityInfo.settings.Insert(u"no_timed_spawn", true); auto* fish = Game::entityManager->CreateEntity(entityInfo); Game::entityManager->ConstructEntity(fish); diff --git a/dScripts/SpawnPetBaseServer.cpp b/dScripts/SpawnPetBaseServer.cpp index d2374162..04a24ae6 100644 --- a/dScripts/SpawnPetBaseServer.cpp +++ b/dScripts/SpawnPetBaseServer.cpp @@ -26,12 +26,10 @@ void SpawnPetBaseServer::OnUse(Entity* self, Entity* user) { info.rot = spawner->GetRotation(); info.lot = self->GetVar<LOT>(u"petLOT"); info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData<LWOOBJID>(u"tamer", user->GetObjectID()), - new LDFData<std::u16string>(u"groupID", petType + (GeneralUtils::to_u16string(user->GetObjectID())) + u";" + petType + u"s"), - new LDFData<std::u16string>(u"spawnAnim", self->GetVar<std::u16string>(u"spawnAnim")), - new LDFData<float_t>(u"spawnTimer", 1.0f) - }; + info.settings.Insert<LWOOBJID>(u"tamer", user->GetObjectID()); + info.settings.Insert<std::u16string>(u"groupID", petType + (GeneralUtils::to_u16string(user->GetObjectID())) + u";" + petType + u"s"); + info.settings.Insert<std::u16string>(u"spawnAnim", self->GetVar<std::u16string>(u"spawnAnim")); + info.settings.Insert<float_t>(u"spawnTimer", 1.0f); auto* pet = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(pet); diff --git a/dScripts/ai/FV/FvPandaSpawnerServer.cpp b/dScripts/ai/FV/FvPandaSpawnerServer.cpp index bc9f1c8a..e02bcb55 100644 --- a/dScripts/ai/FV/FvPandaSpawnerServer.cpp +++ b/dScripts/ai/FV/FvPandaSpawnerServer.cpp @@ -38,10 +38,8 @@ void FvPandaSpawnerServer::OnCollisionPhantom(Entity* self, Entity* target) { info.spawnerID = target->GetObjectID(); info.pos = self->GetPosition(); info.lot = 5643; - info.settings = { - new LDFData<LWOOBJID>(u"tamer", target->GetObjectID()), - new LDFData<std::u16string>(u"groupID", u"panda" + (GeneralUtils::to_u16string(target->GetObjectID())) + u";pandas") - }; + info.settings.Insert<LWOOBJID>(u"tamer", target->GetObjectID()); + info.settings.Insert<std::u16string>(u"groupID", u"panda" + (GeneralUtils::to_u16string(target->GetObjectID())) + u";pandas"); auto* panda = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(panda); diff --git a/dScripts/ai/GF/GfBanana.cpp b/dScripts/ai/GF/GfBanana.cpp index 0b436396..51c9f230 100644 --- a/dScripts/ai/GF/GfBanana.cpp +++ b/dScripts/ai/GF/GfBanana.cpp @@ -66,7 +66,7 @@ void GfBanana::OnHit(Entity* self, Entity* attacker) { info.pos.z -= QuatUtils::Right(rotation).z * 5; info.rot = rotation; info.spawnerID = self->GetObjectID(); - info.settings = { new LDFData<uint32_t>(u"motionType", 5) }; + info.settings.Insert<uint32_t>(u"motionType", 5); auto* const newEn = Game::entityManager->CreateEntity(info, nullptr, self); Game::entityManager->ConstructEntity(newEn); } diff --git a/dScripts/ai/GF/PetDigBuild.cpp b/dScripts/ai/GF/PetDigBuild.cpp index 9caf2f8c..b720907e 100644 --- a/dScripts/ai/GF/PetDigBuild.cpp +++ b/dScripts/ai/GF/PetDigBuild.cpp @@ -13,14 +13,12 @@ void PetDigBuild::OnQuickBuildComplete(Entity* self, Entity* target) { info.pos = pos; info.rot = self->GetRotation(); info.spawnerID = self->GetSpawnerID(); - info.settings = { - new LDFData<LWOOBJID>(u"builder", target->GetObjectID()), - new LDFData<LWOOBJID>(u"X", self->GetObjectID()) - }; + info.settings.Insert<LWOOBJID>(u"builder", target->GetObjectID()); + info.settings.Insert<LWOOBJID>(u"X", self->GetObjectID()); if (!flagNumber.empty()) { info.lot = 7410; // Normal GF treasure - info.settings.push_back(new LDFData<std::u16string>(u"groupID", u"Flag" + flagNumber)); + info.settings.Insert<std::u16string>(u"groupID", u"Flag" + flagNumber); } else { auto* missionComponent = target->GetComponent<MissionComponent>(); if (missionComponent != nullptr && missionComponent->GetMissionState(746) == eMissionState::ACTIVE) { diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp index 3dd2773f..473cd7c5 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp @@ -261,15 +261,13 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { info.spawnerID = self->GetObjectID(); info.pos = path->pathWaypoints[0].position; - info.settings = { - new LDFData<SGEnemy>(u"SpawnData", toSpawn), - new LDFData<std::string>(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"), // this script is never loaded - new LDFData<std::string>(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua"), - new LDFData<std::string>(u"attached_path", path->pathName), - new LDFData<uint32_t>(u"attached_path_start", 0), - new LDFData<std::u16string>(u"groupID", u"SGEnemy"), - new LDFData<uint32_t>(u"wave", self->GetVar<uint32_t>(ThisWaveVariable)), - }; + info.settings.Insert<SGEnemy>(u"SpawnData", toSpawn); + info.settings.Insert<std::string>(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // this script is never loaded; + info.settings.Insert<std::string>(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua"); + info.settings.Insert<std::string>(u"attached_path", path->pathName); + info.settings.Insert<uint32_t>(u"attached_path_start", 0); + info.settings.Insert<std::u16string>(u"groupID", u"SGEnemy"); + info.settings.Insert<uint32_t>(u"wave", self->GetVar<uint32_t>(ThisWaveVariable)); auto* enemy = Game::entityManager->CreateEntity(info, nullptr, self); @@ -621,10 +619,11 @@ void SGCannon::OnActivityNotify(Entity* self, GameMessages::ActivityNotify& noti if (!self->GetVar<bool>(GameStartedVariable)) return; const auto& params = notify.notification; - if (params.empty()) return; + const auto itr = params.values.find(u"shot_done"); + if (itr == params.values.end()) return; - const auto& param = params[0]; - if (param->GetValueType() != LDF_TYPE_S32 || param->GetKey() != u"shot_done") return; + const auto& param = itr->second; + if (param->GetValueType() != LDF_TYPE_S32) return; const auto superChargeShotDone = static_cast<LDFData<int32_t>*>(param.get())->GetValue() == GetConstants().cannonSuperChargeSkill; diff --git a/dScripts/ai/NS/WH/RockHydrantSmashable.cpp b/dScripts/ai/NS/WH/RockHydrantSmashable.cpp index d388baac..6549a85b 100644 --- a/dScripts/ai/NS/WH/RockHydrantSmashable.cpp +++ b/dScripts/ai/NS/WH/RockHydrantSmashable.cpp @@ -6,13 +6,11 @@ void RockHydrantSmashable::OnDie(Entity* self, Entity* killer) { const auto hydrantName = self->GetVar<std::u16string>(u"hydrant"); - LDFBaseData* data = new LDFData<std::string>(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); - EntityInfo info{}; info.lot = ROCK_HYDRANT_BROKEN; info.pos = self->GetPosition(); info.rot = self->GetRotation(); - info.settings = { data }; + info.settings.Insert<std::string>(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); info.spawnerID = self->GetSpawnerID(); auto* hydrant = Game::entityManager->CreateEntity(info); diff --git a/dScripts/ai/PETS/HydrantSmashable.cpp b/dScripts/ai/PETS/HydrantSmashable.cpp index fc83a5d3..9e770615 100644 --- a/dScripts/ai/PETS/HydrantSmashable.cpp +++ b/dScripts/ai/PETS/HydrantSmashable.cpp @@ -6,13 +6,11 @@ void HydrantSmashable::OnDie(Entity* self, Entity* killer) { const auto hydrantName = self->GetVar<std::u16string>(u"hydrant"); - LDFBaseData* data = new LDFData<std::string>(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); - EntityInfo info{}; info.lot = HYDRANT_BROKEN; info.pos = self->GetPosition(); info.rot = self->GetRotation(); - info.settings = { data }; + info.settings.Insert<std::string>(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); info.spawnerID = self->GetSpawnerID(); auto* hydrant = Game::entityManager->CreateEntity(info); diff --git a/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp b/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp index 5dd43f48..9c53a990 100644 --- a/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp @@ -10,46 +10,42 @@ void FvRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"GameType", u"Racing")); - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"GameState", u"Starting")); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Minimum_Players_for_Group_Achievements", 2)); + raceSet.Insert<std::u16string>(u"GameType", u"Racing"); + raceSet.Insert<std::u16string>(u"GameState", u"Starting"); + raceSet.Insert<int32_t>(u"Number_Of_PlayersPerTeam", 6); + raceSet.Insert<int32_t>(u"Minimum_Players_to_Start", 2); + raceSet.Insert<int32_t>(u"Minimum_Players_for_Group_Achievements", 2); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Car_Object", 7703)); - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"Race_PathName", u"MainPath")); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Current_Lap", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_of_Laps", 3)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"activityID", 54)); + raceSet.Insert<int32_t>(u"Car_Object", 7703); + raceSet.Insert<std::u16string>(u"Race_PathName", u"MainPath"); + raceSet.Insert<int32_t>(u"Current_Lap", 1); + raceSet.Insert<int32_t>(u"Number_of_Laps", 3); + raceSet.Insert<int32_t>(u"activityID", 54); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_1", 100)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_2", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_3", 80)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_4", 70)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_5", 60)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_6", 50)); + raceSet.Insert<int32_t>(u"Place_1", 100); + raceSet.Insert<int32_t>(u"Place_2", 90); + raceSet.Insert<int32_t>(u"Place_3", 80); + raceSet.Insert<int32_t>(u"Place_4", 70); + raceSet.Insert<int32_t>(u"Place_5", 60); + raceSet.Insert<int32_t>(u"Place_6", 50); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_1", 15)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_2", 25)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_3", 50)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_4", 85)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_5", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_6", 100)); + raceSet.Insert<int32_t>(u"Num_of_Players_1", 15); + raceSet.Insert<int32_t>(u"Num_of_Players_2", 25); + raceSet.Insert<int32_t>(u"Num_of_Players_3", 50); + raceSet.Insert<int32_t>(u"Num_of_Players_4", 85); + raceSet.Insert<int32_t>(u"Num_of_Players_5", 90); + raceSet.Insert<int32_t>(u"Num_of_Players_6", 100); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Spawners", 4847)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Spawners", 4848)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Flag", 4850)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Flag", 4851)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Point", 4846)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Point", 4845)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Mark", 4844)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Mark", 4843)); + raceSet.Insert<int32_t>(u"Number_of_Spawn_Groups", 1); + raceSet.Insert<int32_t>(u"Red_Spawners", 4847); + raceSet.Insert<int32_t>(u"Blue_Spawners", 4848); + raceSet.Insert<int32_t>(u"Blue_Flag", 4850); + raceSet.Insert<int32_t>(u"Red_Flag", 4851); + raceSet.Insert<int32_t>(u"Red_Point", 4846); + raceSet.Insert<int32_t>(u"Blue_Point", 4845); + raceSet.Insert<int32_t>(u"Red_Mark", 4844); + raceSet.Insert<int32_t>(u"Blue_Mark", 4843); - const std::vector<Entity*> racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* racingComponent = racingController->GetComponent<RacingControlComponent>(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp b/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp index d465d57f..c7a4cf94 100644 --- a/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp @@ -10,46 +10,42 @@ void GfRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"GameType", u"Racing")); - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"GameState", u"Starting")); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Minimum_Players_for_Group_Achievements", 2)); + raceSet.Insert<std::u16string>(u"GameType", u"Racing"); + raceSet.Insert<std::u16string>(u"GameState", u"Starting"); + raceSet.Insert<int32_t>(u"Number_Of_PlayersPerTeam", 6); + raceSet.Insert<int32_t>(u"Minimum_Players_to_Start", 2); + raceSet.Insert<int32_t>(u"Minimum_Players_for_Group_Achievements", 2); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Car_Object", 7703)); - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"Race_PathName", u"MainPath")); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Current_Lap", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_of_Laps", 3)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"activityID", 39)); + raceSet.Insert<int32_t>(u"Car_Object", 7703); + raceSet.Insert<std::u16string>(u"Race_PathName", u"MainPath"); + raceSet.Insert<int32_t>(u"Current_Lap", 1); + raceSet.Insert<int32_t>(u"Number_of_Laps", 3); + raceSet.Insert<int32_t>(u"activityID", 39); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_1", 100)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_2", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_3", 80)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_4", 70)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_5", 60)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_6", 50)); + raceSet.Insert<int32_t>(u"Place_1", 100); + raceSet.Insert<int32_t>(u"Place_2", 90); + raceSet.Insert<int32_t>(u"Place_3", 80); + raceSet.Insert<int32_t>(u"Place_4", 70); + raceSet.Insert<int32_t>(u"Place_5", 60); + raceSet.Insert<int32_t>(u"Place_6", 50); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_1", 15)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_2", 25)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_3", 50)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_4", 85)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_5", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_6", 100)); + raceSet.Insert<int32_t>(u"Num_of_Players_1", 15); + raceSet.Insert<int32_t>(u"Num_of_Players_2", 25); + raceSet.Insert<int32_t>(u"Num_of_Players_3", 50); + raceSet.Insert<int32_t>(u"Num_of_Players_4", 85); + raceSet.Insert<int32_t>(u"Num_of_Players_5", 90); + raceSet.Insert<int32_t>(u"Num_of_Players_6", 100); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Spawners", 4847)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Spawners", 4848)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Flag", 4850)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Flag", 4851)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Point", 4846)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Point", 4845)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Mark", 4844)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Mark", 4843)); + raceSet.Insert<int32_t>(u"Number_of_Spawn_Groups", 1); + raceSet.Insert<int32_t>(u"Red_Spawners", 4847); + raceSet.Insert<int32_t>(u"Blue_Spawners", 4848); + raceSet.Insert<int32_t>(u"Blue_Flag", 4850); + raceSet.Insert<int32_t>(u"Red_Flag", 4851); + raceSet.Insert<int32_t>(u"Red_Point", 4846); + raceSet.Insert<int32_t>(u"Blue_Point", 4845); + raceSet.Insert<int32_t>(u"Red_Mark", 4844); + raceSet.Insert<int32_t>(u"Blue_Mark", 4843); - const std::vector<Entity*> racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* racingComponent = racingController->GetComponent<RacingControlComponent>(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp b/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp index 5ef6c6d1..3c9efa3a 100644 --- a/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp @@ -10,45 +10,41 @@ void NsRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"GameType", u"Racing")); - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"GameState", u"Starting")); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Minimum_Players_for_Group_Achievements", 2)); + raceSet.Insert<std::u16string>(u"GameType", u"Racing"); + raceSet.Insert<std::u16string>(u"GameState", u"Starting"); + raceSet.Insert<int32_t>(u"Number_Of_PlayersPerTeam", 6); + raceSet.Insert<int32_t>(u"Minimum_Players_to_Start", 2); + raceSet.Insert<int32_t>(u"Minimum_Players_for_Group_Achievements", 2); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Car_Object", 7703)); - raceSet.push_back(make_unique<LDFData<std::u16string>>(u"Race_PathName", u"MainPath")); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Current_Lap", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_of_Laps", 3)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"activityID", 42)); + raceSet.Insert<int32_t>(u"Car_Object", 7703); + raceSet.Insert<std::u16string>(u"Race_PathName", u"MainPath"); + raceSet.Insert<int32_t>(u"Current_Lap", 1); + raceSet.Insert<int32_t>(u"Number_of_Laps", 3); + raceSet.Insert<int32_t>(u"activityID", 42); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_1", 100)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_2", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_3", 80)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_4", 70)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_5", 60)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Place_6", 50)); + raceSet.Insert<int32_t>(u"Place_1", 100); + raceSet.Insert<int32_t>(u"Place_2", 90); + raceSet.Insert<int32_t>(u"Place_3", 80); + raceSet.Insert<int32_t>(u"Place_4", 70); + raceSet.Insert<int32_t>(u"Place_5", 60); + raceSet.Insert<int32_t>(u"Place_6", 50); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_1", 15)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_2", 25)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_3", 50)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_4", 85)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_5", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Num_of_Players_6", 100)); + raceSet.Insert<int32_t>(u"Num_of_Players_1", 15); + raceSet.Insert<int32_t>(u"Num_of_Players_2", 25); + raceSet.Insert<int32_t>(u"Num_of_Players_3", 50); + raceSet.Insert<int32_t>(u"Num_of_Players_4", 85); + raceSet.Insert<int32_t>(u"Num_of_Players_5", 90); + raceSet.Insert<int32_t>(u"Num_of_Players_6", 100); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Spawners", 4847)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Spawners", 4848)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Flag", 4850)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Flag", 4851)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Point", 4846)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Point", 4845)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Red_Mark", 4844)); - raceSet.push_back(make_unique<LDFData<int32_t>>(u"Blue_Mark", 4843)); + raceSet.Insert<int32_t>(u"Number_of_Spawn_Groups", 1); + raceSet.Insert<int32_t>(u"Red_Spawners", 4847); + raceSet.Insert<int32_t>(u"Blue_Spawners", 4848); + raceSet.Insert<int32_t>(u"Blue_Flag", 4850); + raceSet.Insert<int32_t>(u"Red_Flag", 4851); + raceSet.Insert<int32_t>(u"Red_Point", 4846); + raceSet.Insert<int32_t>(u"Blue_Point", 4845); + raceSet.Insert<int32_t>(u"Red_Mark", 4844); + raceSet.Insert<int32_t>(u"Blue_Mark", 4843); - std::vector<Entity*> racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* racingComponent = racingController->GetComponent<RacingControlComponent>(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp b/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp index 9a7712a1..8baaa4c6 100644 --- a/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp @@ -9,45 +9,41 @@ void NsWinterRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique<LDFData<std::u16string>>("GameType", u"Racing")); - raceSet.push_back(make_unique<LDFData<std::u16string>>("GameState", u"Starting")); - raceSet.push_back(make_unique<LDFData<int32_t>>("Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Minimum_Players_for_Group_Achievments", 2)); + raceSet.Insert<std::u16string>("GameType", u"Racing"); + raceSet.Insert<std::u16string>("GameState", u"Starting"); + raceSet.Insert<int32_t>("Number_Of_PlayersPerTeam", 6); + raceSet.Insert<int32_t>("Minimum_Players_to_Start", 2); + raceSet.Insert<int32_t>("Minimum_Players_for_Group_Achievments", 2); - raceSet.push_back(make_unique<LDFData<int32_t>>("Car_Object", 7703)); - raceSet.push_back(make_unique<LDFData<std::u16string>>("Race_PathName", u"MainPath")); - raceSet.push_back(make_unique<LDFData<int32_t>>("Current_Lap", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Number_of_Laps", 3)); - raceSet.push_back(make_unique<LDFData<int32_t>>("activityID", 60)); + raceSet.Insert<int32_t>("Car_Object", 7703); + raceSet.Insert<std::u16string>("Race_PathName", u"MainPath"); + raceSet.Insert<int32_t>("Current_Lap", 1); + raceSet.Insert<int32_t>("Number_of_Laps", 3); + raceSet.Insert<int32_t>("activityID", 60); - raceSet.push_back(make_unique<LDFData<int32_t>>("Place_1", 100)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Place_2", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Place_3", 80)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Place_4", 70)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Place_5", 60)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Place_6", 50)); + raceSet.Insert<int32_t>("Place_1", 100); + raceSet.Insert<int32_t>("Place_2", 90); + raceSet.Insert<int32_t>("Place_3", 80); + raceSet.Insert<int32_t>("Place_4", 70); + raceSet.Insert<int32_t>("Place_5", 60); + raceSet.Insert<int32_t>("Place_6", 50); - raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_1", 15)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_2", 25)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_3", 50)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_4", 85)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_5", 90)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_6", 100)); + raceSet.Insert<int32_t>("Num_of_Players_1", 15); + raceSet.Insert<int32_t>("Num_of_Players_2", 25); + raceSet.Insert<int32_t>("Num_of_Players_3", 50); + raceSet.Insert<int32_t>("Num_of_Players_4", 85); + raceSet.Insert<int32_t>("Num_of_Players_5", 90); + raceSet.Insert<int32_t>("Num_of_Players_6", 100); - raceSet.push_back(make_unique<LDFData<int32_t>>("Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Spawners", 4847)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Spawners", 4848)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Flag", 4850)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Flag", 4851)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Point", 4846)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Point", 4845)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Mark", 4844)); - raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Mark", 4843)); + raceSet.Insert<int32_t>("Number_of_Spawn_Groups", 1); + raceSet.Insert<int32_t>("Red_Spawners", 4847); + raceSet.Insert<int32_t>("Blue_Spawners", 4848); + raceSet.Insert<int32_t>("Blue_Flag", 4850); + raceSet.Insert<int32_t>("Red_Flag", 4851); + raceSet.Insert<int32_t>("Red_Point", 4846); + raceSet.Insert<int32_t>("Blue_Point", 4845); + raceSet.Insert<int32_t>("Red_Mark", 4844); + raceSet.Insert<int32_t>("Blue_Mark", 4843); - const auto racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* const racingComponent = racingController->GetComponent<RacingControlComponent>(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index 40ee6d30..72fbdcdd 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -16,6 +16,7 @@ #include "AssetManager.h" #include "ClientVersion.h" #include "dConfig.h" +#include <ranges> Level::Level(Zone* parentZone, const std::string& filepath) { m_ParentZone = parentZone; @@ -30,7 +31,7 @@ Level::Level(Zone* parentZone, const std::string& filepath) { ReadChunks(stream); } -void Level::MakeSpawner(SceneObject obj) { +void Level::MakeSpawner(const SceneObject& obj) { SpawnerInfo spawnInfo = SpawnerInfo(); SpawnerNode* node = new SpawnerNode(); spawnInfo.templateID = obj.lot; @@ -40,7 +41,7 @@ void Level::MakeSpawner(SceneObject obj) { node->rotation = obj.rotation; node->config = obj.settings; spawnInfo.nodes.push_back(node); - for (LDFBaseData* data : obj.settings) { + for (const auto& data : obj.settings.values | std::views::values) { if (!data) continue; if (data->GetKey() == u"spawntemplate") { spawnInfo.templateID = GeneralUtils::TryParse(data->GetValueAsString(), 0); @@ -70,7 +71,7 @@ void Level::MakeSpawner(SceneObject obj) { if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds { spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0.0f); - } else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms? + } else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms { spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0) / 1000; } @@ -86,13 +87,13 @@ void Level::MakeSpawner(SceneObject obj) { if (spawnInfo.groups.back().empty()) spawnInfo.groups.erase(spawnInfo.groups.end() - 1); } if (data->GetKey() == u"no_auto_spawn") { - spawnInfo.noAutoSpawn = static_cast<LDFData<bool>*>(data)->GetValue(); + spawnInfo.noAutoSpawn = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"no_timed_spawn") { - spawnInfo.noTimedSpawn = static_cast<LDFData<bool>*>(data)->GetValue(); + spawnInfo.noTimedSpawn = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"spawnActivator") { - spawnInfo.spawnActivator = static_cast<LDFData<bool>*>(data)->GetValue(); + spawnInfo.spawnActivator = GeneralUtils::TryParse(data->GetValueAsString(), false); } } @@ -257,20 +258,14 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation); } - std::string sData = GeneralUtils::UTF16ToWTF8(ldfString); - std::stringstream ssData(sData); - std::string token; - char deliminator = '\n'; - - while (std::getline(ssData, token, deliminator)) { - LDFBaseData* ldfData = LDFBaseData::DataFromString(token); - obj.settings.push_back(ldfData); + for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) { + obj.settings.ParseInsert(token); } // We should never have more than 1 zone control object bool skipLoadingObject = obj.lot == zoneControlObject->GetLOT(); - for (LDFBaseData* data : obj.settings) { + for (const auto& data : obj.settings | std::views::values) { if (!data) continue; if (data->GetKey() == u"gatingOnFeature") { gating.featureName = data->GetValueAsString(); @@ -296,11 +291,6 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { } if (skipLoadingObject) { - for (auto* setting : obj.settings) { - delete setting; - setting = nullptr; - } - continue; } diff --git a/dZoneManager/Level.h b/dZoneManager/Level.h index 1f0b4f2e..22860fe7 100644 --- a/dZoneManager/Level.h +++ b/dZoneManager/Level.h @@ -40,7 +40,7 @@ public: public: Level(Zone* parentZone, const std::string& filepath); - static void MakeSpawner(SceneObject obj); + static void MakeSpawner(const SceneObject& obj); std::map<uint32_t, Header> m_ChunkHeaders; private: diff --git a/dZoneManager/Spawner.h b/dZoneManager/Spawner.h index 701ceb29..3f01161d 100644 --- a/dZoneManager/Spawner.h +++ b/dZoneManager/Spawner.h @@ -17,7 +17,7 @@ struct SpawnerNode { uint32_t nodeID = 0; uint32_t nodeMax = 1; std::vector<LWOOBJID> entities; - std::vector<LDFBaseData*> config; + LwoNameValue config; }; struct SpawnerInfo { diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 282f4c45..893d9598 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -111,7 +111,7 @@ void Zone::LoadZoneIntoMemory() { node->nodeID = 0; node->config = waypoint.config; - for (LDFBaseData* data : waypoint.config) { + for (const auto& data : waypoint.config | std::views::values) { if (!data) continue; if (data->GetKey() == u"spawner_node_id") { @@ -465,7 +465,7 @@ void Zone::LoadPath(std::istream& file) { command.data = value; } else LOG("Tried to load invalid waypoint command '%s'", parameter.c_str()); } else { - waypoint.config.emplace_back(LDFBaseData::DataFromString(parameter + "=" + value)); + waypoint.config.ParseInsert(parameter + "=" + value); } } diff --git a/dZoneManager/Zone.h b/dZoneManager/Zone.h index 20010c2c..4cb74efa 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -76,7 +76,7 @@ struct PathWaypoint { CameraPathWaypoint camera; RacingPathWaypoint racing; float speed{}; - std::vector<LDFBaseData*> config; + LwoNameValue config; std::vector<WaypointCommand> commands; }; diff --git a/dZoneManager/dZMCommon.h b/dZoneManager/dZMCommon.h index 5acdc6b7..53f1d3c4 100644 --- a/dZoneManager/dZMCommon.h +++ b/dZoneManager/dZMCommon.h @@ -13,9 +13,8 @@ struct SceneObject { NiPoint3 position; NiQuaternion rotation = QuatUtils::IDENTITY; float scale = 1.0f; - //std::string settings; uint32_t value3; - std::vector<LDFBaseData*> settings; + LwoNameValue settings; }; #define LOT_MARKER_PLAYER_START 1931 diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index fe72f2da..61c13de2 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -70,7 +70,7 @@ endif() FetchContent_Declare( glm GIT_REPOSITORY https://github.com/g-truc/glm.git - GIT_TAG bf71a834948186f4097caa076cd2663c69a10e1e #refs/tags/1.0.1 + GIT_TAG 8d1fd52e5ab5590e2c81768ace50c72bae28f2ed #refs/tags/1.0.3 GIT_PROGRESS TRUE GIT_SHALLOW 1 ) diff --git a/thirdparty/recastnavigation b/thirdparty/recastnavigation index c5cbd530..9f4ce644 160000 --- a/thirdparty/recastnavigation +++ b/thirdparty/recastnavigation @@ -1 +1 @@ -Subproject commit c5cbd53024c8a9d8d097a4371215e3342d2fdc87 +Subproject commit 9f4ce64458dfae86e1239c525ddc219c4e9e06f1 From 7937951f7ff8f20a613aa7a5d16e850334106b5c Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 15 Jun 2026 00:20:43 -0700 Subject: [PATCH 56/69] fix: mech build not showing up (#1996) tested that the mech build, the fv rock build, and the frakjaw builds (to get to the instancer) all function as intented --- dGame/Entity.cpp | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 25ffb1e4..c4265470 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -1615,23 +1615,8 @@ void Entity::Kill(Entity* murderer, const eKillType killType) { const auto& grpNameQBShowBricks = GetVarAsString(u"grpNameQBShowBricks"); if (!grpNameQBShowBricks.empty()) { - auto spawners = Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks); - - Spawner* spawner = nullptr; - - if (!spawners.empty()) { - spawner = spawners[0]; - } else { - spawners = Game::zoneManager->GetSpawnersInGroup(grpNameQBShowBricks); - - if (!spawners.empty()) { - spawner = spawners[0]; - } - } - - if (spawner != nullptr) { - 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(); } // Track a player being smashed From c0d055e66ea8d3d872f9f57396731071d407e63f Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:44:24 -0700 Subject: [PATCH 57/69] fix dragon fire trails (#1997) tested that the fire breath attack works now (hack fix) --- dGame/dBehaviors/TacArcBehavior.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dGame/dBehaviors/TacArcBehavior.cpp b/dGame/dBehaviors/TacArcBehavior.cpp index bea246aa..076fc515 100644 --- a/dGame/dBehaviors/TacArcBehavior.cpp +++ b/dGame/dBehaviors/TacArcBehavior.cpp @@ -207,6 +207,10 @@ void TacArcBehavior::Load() { GetFloat("offset_y", 0.0f), GetFloat("offset_z", 0.0f) ); + // https://explorer.lu/skills/behaviors/6212/6203 HACK: i cant figure out why the dragon fire wall doesnt work with the offset, probably has to be fixed with the near/far height parameters + if (m_behaviorId == 6203) { + this->m_offset = NiPoint3Constant::ZERO; + } this->m_method = GetInt("method", 1); this->m_upperBound = GetFloat("upper_bound", 4.4f); this->m_lowerBound = GetFloat("lower_bound", 0.4f) - 5.0f; // Makes it so players and objects can still be targetted when slightly below the caster. FIXME: use bounding spheres at some point From 79bb48d3bcc7ca6185f704160680be678ba5bab5 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 16 Jun 2026 07:49:30 -0700 Subject: [PATCH 58/69] feat: implement a bunch of basic scripts that don't really do anything (#1999) * feat: implement a bunch of basic scripts that don't really do anything None of these do anything noticeable or break anything * fixes --- dGame/dGameMessages/GameMessages.h | 7 +++++++ dScripts/02_server/DLU/CMakeLists.txt | 1 + dScripts/02_server/DLU/RegisterWithZoneControl.cpp | 12 ++++++++++++ dScripts/02_server/DLU/RegisterWithZoneControl.h | 14 ++++++++++++++ dScripts/02_server/Map/njhub/OldManNPC.cpp | 2 -- dScripts/CppScripts.cpp | 14 ++++++++++++-- 6 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 dScripts/02_server/DLU/RegisterWithZoneControl.cpp create mode 100644 dScripts/02_server/DLU/RegisterWithZoneControl.h diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 8937e028..eef568d3 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -962,5 +962,12 @@ namespace GameMessages { LWOOBJID childID{}; }; + + struct ObjectLoaded : public GameMsg { + ObjectLoaded() : GameMsg(MessageType::Game::OBJECT_LOADED) {} + + LWOOBJID objectID{}; + LOT lot{}; + }; }; #endif // GAMEMESSAGES_H diff --git a/dScripts/02_server/DLU/CMakeLists.txt b/dScripts/02_server/DLU/CMakeLists.txt index fb257d3e..3185df56 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 00000000..e588f240 --- /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 00000000..dda89882 --- /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/Map/njhub/OldManNPC.cpp b/dScripts/02_server/Map/njhub/OldManNPC.cpp index 4d1f1056..8a649389 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<MissionComponent>(); 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/CppScripts.cpp b/dScripts/CppScripts.cpp index 6300716f..a1f37153 100644 --- a/dScripts/CppScripts.cpp +++ b/dScripts/CppScripts.cpp @@ -339,6 +339,8 @@ #include "ImaginationBackPack.h" #include "NsWinterRaceServer.h" +#include "RegisterWithZoneControl.h" + #include <map> #include <string> #include <functional> @@ -663,6 +665,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 +711,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<std::string> g_ExcludedScripts = { @@ -732,6 +736,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 +754,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; From c898356eba5217933ba4e3173b8242461b3b24b2 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 16 Jun 2026 07:49:56 -0700 Subject: [PATCH 59/69] fix: enemies not interrupting QB's when they do damage (#1998) tested that stromlings in AG now correctly interrupt quickbuilds if the player takes damage --- dGame/dBehaviors/NpcCombatSkillBehavior.h | 2 ++ dGame/dBehaviors/VerifyBehavior.cpp | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dGame/dBehaviors/NpcCombatSkillBehavior.h b/dGame/dBehaviors/NpcCombatSkillBehavior.h index 07826f48..993aed76 100644 --- a/dGame/dBehaviors/NpcCombatSkillBehavior.h +++ b/dGame/dBehaviors/NpcCombatSkillBehavior.h @@ -8,6 +8,8 @@ public: float m_npcSkillTime; + float m_maxRange{}; + /* * Inherited */ diff --git a/dGame/dBehaviors/VerifyBehavior.cpp b/dGame/dBehaviors/VerifyBehavior.cpp index c7ede52f..ec4b27a8 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 } From 0f17e1de3b644177125724f1629c9e5cf5f4980a Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:07:36 -0700 Subject: [PATCH 60/69] feat: enemy npc pathing (#2000) * feat: enemy npc pathing they live :tada: tested that enemies path all around the world should they have a path configured. tested that the admiral in gf (at the first camp) paths now. fixes #1546 * feedback --- dGame/dComponents/BaseCombatAIComponent.cpp | 23 ++++-- dGame/dComponents/BaseCombatAIComponent.h | 2 + dGame/dComponents/MovementAIComponent.cpp | 78 +++++++++++++++++++-- dGame/dComponents/MovementAIComponent.h | 8 +++ dZoneManager/Zone.cpp | 16 +++-- 5 files changed, 111 insertions(+), 16 deletions(-) diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index b97211b8..ed4bf70d 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -210,8 +210,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; } @@ -317,12 +319,14 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { SetAiState(AiState::aggro); } else { SetAiState(AiState::idle); + if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); } if (!hasSkillToCast) return; if (m_Target == LWOOBJID_EMPTY) { SetAiState(AiState::idle); + if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); return; } @@ -618,6 +622,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(); @@ -862,12 +871,12 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport // roundInfo.PushDebug<AMFDoubleValue>("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<AMFStringValue>("Current Combat State") = curState; diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 95eb75ce..3485ac16 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -236,6 +236,8 @@ public: bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + void SetStartingPosition(const NiPoint3& pos) { m_StartPosition = pos; } + private: /** * Returns the current target or the target that currently is the largest threat to this entity diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index 3519712a..e885b4bb 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 { /** @@ -60,7 +66,7 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo); - if (!m_Parent->GetComponent<BaseCombatAIComponent>()) SetPath(m_Parent->GetVarAsString(u"attached_path")); + SetPath(m_Parent->GetVarAsString(u"attached_path")); } void MovementAIComponent::SetPath(const std::string pathName) { @@ -162,6 +168,7 @@ void MovementAIComponent::Update(const float deltaTime) { } 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; + RunWaypointCommands(waypointNum); if (m_CurrentPath.empty()) { if (m_Path) { if (m_Path->pathBehavior == PathBehavior::Loop) { @@ -172,17 +179,16 @@ void MovementAIComponent::Update(const float deltaTime) { if (m_IsBounced) std::ranges::reverse(waypoints); SetPath(waypoints); } else if (m_Path->pathBehavior == PathBehavior::Once) { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + // In this case we intended to follow a path and once we've followed it we camp there, otherwise we'd just wander home again. + if (m_BaseCombatAI) m_BaseCombatAI->SetStartingPosition(m_SourcePosition); Stop(); return; } } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); Stop(); return; } } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); SetDestination(m_CurrentPath.top().position); m_CurrentPath.pop(); @@ -423,7 +429,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<uint32_t>(data); + if (skill) { + auto* const skillComponent = m_Parent->GetComponent<SkillComponent>(); + if (skillComponent) skillComponent->CastSkill(skill.value()); + } + break; + } + case eWaypointCommandType::EQUIP_INVENTORY: { + auto* const inventoryComponent = m_Parent->GetComponent<InventoryComponent>(); + 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<InventoryComponent>(); + 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<float>(data).value_or(0.0f)); + break; + } + case eWaypointCommandType::EMOTE: { + // m_Delay = RenderComponent::GetAnimationTime(m_Parent, data); + // const auto emoteID = GeneralUtils::TryParse<uint32_t>(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<float>(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) { diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 2a095716..af7da7f0 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -212,8 +212,16 @@ public: bool IsPaused() const { return m_Paused; } bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + + bool HasPath() const { return m_Path != nullptr; } 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 diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 893d9598..fcb755f0 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -101,15 +101,23 @@ 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; From ce9d4e823c09cfdca52cc6625dfaf0f7cd140dfc Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:27:14 -0700 Subject: [PATCH 61/69] feat: enemies now use weights on their attacks (#2004) * feat: enemies now use weights on their attacks tested that 8 times out of 10, in close range, spiders did a web attack instead of a melee attack, vs the prior behavior of always following a pattern fixes #2002 * feedback --- .../CDClientTables/CDObjectSkillsTable.cpp | 8 ++ .../CDClientTables/CDObjectSkillsTable.h | 4 +- dGame/dComponents/BaseCombatAIComponent.cpp | 74 +++++++++++-------- dGame/dComponents/BaseCombatAIComponent.h | 18 ++++- 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp index a07446b5..522c3e88 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp @@ -38,3 +38,11 @@ std::vector<CDObjectSkills> CDObjectSkillsTable::Query(std::function<bool(CDObje return data; } + +std::vector<CDObjectSkills> CDObjectSkillsTable::Get(const LOT lot) const { + std::vector<CDObjectSkills> toReturn; + for (const auto& entry : GetEntries()) { + if (entry.objectTemplate == lot) toReturn.push_back(entry); + } + return toReturn; +} diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h index 731f6657..ed314212 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h @@ -4,12 +4,13 @@ #include "CDTable.h" #include <cstdint> +#include <vector> 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<CDObjectSkillsTable, std::vector<CDObjectSkills>> { @@ -17,5 +18,6 @@ public: void LoadValuesFromDatabase(); // Queries the table with a custom "where" clause std::vector<CDObjectSkills> Query(std::function<bool(CDObjectSkills)> predicate); + std::vector<CDObjectSkills> Get(const LOT lot) const; }; diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index ed4bf70d..d4d70025 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 <algorithm> @@ -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<int>(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"); + + m_MinRoundLength = componentResult.getFloatField("minRoundLength"); + m_MaxRoundLength = componentResult.getFloatField("maxRoundLength"); + m_CombatRoundLength = componentResult.getFloatField("combatRoundLength"); } - componentResult.finalize(); - // Get aggro and tether radius from settings and use this if it is present. Only overwrite the // radii if it is greater than the one in the database. - if (m_Parent) { - auto aggroRadius = m_Parent->GetVar<float>(u"aggroRadius"); - m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius; - auto tetherRadius = m_Parent->GetVar<float>(u"tetherRadius"); - m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius; - } + m_AggroRadius = m_Parent->HasVar(u"aggroRadius") ? m_Parent->GetVar<float>(u"aggroRadius") : m_AggroRadius; + m_HardTetherRadius = m_Parent->HasVar(u"tetherRadius") ? m_Parent->GetVar<float>(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<int>(parent->GetLOT())); + for (const auto objectSkill : CDClientManager::GetTable<CDObjectSkillsTable>()->Get(parent->GetLOT())) { + const auto skillBehavior = CDClientManager::GetTable<CDSkillBehaviorTable>()->GetSkillByID(objectSkill.skillID); + if (skillBehavior.skillID == objectSkill.skillID) { + const auto skillId = skillBehavior.skillID; - auto result = skillQuery.execQuery(); + const auto abilityCooldown = skillBehavior.cooldown; - while (!result.eof()) { - const auto skillId = static_cast<uint32_t>(result.getIntField("skillID")); + const auto behaviorId = skillBehavior.behaviorID; - const auto abilityCooldown = static_cast<float>(result.getFloatField("cooldown")); + const auto combatWeight = objectSkill.AICombatWeight; - const auto behaviorId = static_cast<uint32_t>(result.getIntField("behaviorID")); + auto* behavior = Behavior::CreateBehavior(behaviorId); - auto* behavior = Behavior::CreateBehavior(behaviorId); + AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight }; - std::stringstream behaviorQuery; - - AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior }; - - m_SkillEntries.push_back(entry); - - result.nextRow(); + m_SkillEntries.push_back(entry); + } } Stun(1.0f); @@ -248,10 +243,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; } } @@ -337,10 +334,19 @@ 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<int32_t>(0, maxSkillWeights); - if (entry.cooldown > 0) { + for (auto& entry : m_SkillEntries) { + // Skill isn't cooled off yet + if (entry.cooldown > 0.0f) { + continue; + } + + randomizedWeight -= entry.combatWeight; + + // if the weight is still greater than 0 continue to the next rolled skill + if (randomizedWeight > 0) { continue; } @@ -359,8 +365,6 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { entry.cooldown = entry.abilityCooldown + m_SkillTime; - m_SkillEntries[i] = entry; - break; } } @@ -914,8 +918,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<AMFDoubleValue>(std::to_string(id) + " - Time") = threat; } + auto& skillInfo = cmptType.PushDebug("Skill Info"); + for (const auto& skill : m_SkillEntries) { + auto& skillDebug = skillInfo.PushDebug("Skill ID " + std::to_string(skill.skillId)); + skillDebug.PushDebug<AMFDoubleValue>("Cooldown") = skill.cooldown; + skillDebug.PushDebug<AMFDoubleValue>("Ability Cooldown") = skill.abilityCooldown; + skillDebug.PushDebug<AMFIntValue>("AI Combat Weight") = skill.combatWeight; + } + return true; } diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 3485ac16..18de88cb 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{}; }; /** @@ -396,9 +398,17 @@ private: */ 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<LWOOBJID, float> m_RemovedThreatList; From 56504d9447f41b376436e3d50f09e96045c5bb1b Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:27:49 -0700 Subject: [PATCH 62/69] fix: add range checks to npc combat skill behavior (#2003) * fix: add range checks to npc combat skill behavior tested that all enemies now cast skills smartly based on range to targets, and do not cast skills if they are out of range. fixes an issue where the spider queen could attack you outside the normal range fixes an issue where entering happy flower caused you to need to restart the client fixes #965 * feedback --- dGame/dBehaviors/NpcCombatSkillBehavior.cpp | 30 +++++++++++--- dGame/dBehaviors/NpcCombatSkillBehavior.h | 1 + dGame/dComponents/BaseCombatAIComponent.cpp | 6 +-- dGame/dComponents/BaseCombatAIComponent.h | 3 +- .../Enemy/AG/BossSpiderQueenEnemyServer.cpp | 40 ++++++++++++++++++- .../Enemy/AG/BossSpiderQueenEnemyServer.h | 3 ++ dWorldServer/WorldServer.cpp | 1 + 7 files changed, 74 insertions(+), 10 deletions(-) diff --git a/dGame/dBehaviors/NpcCombatSkillBehavior.cpp b/dGame/dBehaviors/NpcCombatSkillBehavior.cpp index 69a6ea9d..433594a2 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 993aed76..a1c7c0e9 100644 --- a/dGame/dBehaviors/NpcCombatSkillBehavior.h +++ b/dGame/dBehaviors/NpcCombatSkillBehavior.h @@ -9,6 +9,7 @@ public: float m_npcSkillTime; float m_maxRange{}; + float m_minRange{}; /* * Inherited diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index d4d70025..89c39c33 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -350,7 +350,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { 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) { @@ -759,8 +759,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; } diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 18de88cb..728e0d4a 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -183,8 +183,9 @@ public: /** * 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 diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp index b3938fd4..869e09ba 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 <vector> @@ -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<int32_t>(u"player_count"); + + if (status == "ENTER") { + if (playerCount == 0) { + ToggleAttacking(*self, true); + } + playerCount++; + } else if (status == "LEAVE") { + playerCount--; + if (playerCount == 0) { + ToggleAttacking(*self, false); + } + } + self->SetVar<int32_t>(u"player_count", playerCount); +} + void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { 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<bool>(u"stoppedFlag"); + + if (!on) { + if (stoppedFlag) return; + + self.SetVar(u"stoppedFlag", true); + combat->Stun(100000.0f, true); // forcibly stun so we stop attacking people trying to put on armor + } else { + if (!stoppedFlag) return; + + self.SetVar(u"stoppedFlag", false); + combat->Stun(0.0f, true); // forcibly turn off the stun we put on above + } +} diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h index b5909000..0f975abe 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h @@ -46,7 +46,10 @@ public: 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/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 8de1d4e5..e4d324f1 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); From 308412f46e66a6d86d8992dc7e0830056bb0991f Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:12:47 -0700 Subject: [PATCH 63/69] fix: enemies snapping to the incorrect position if they had a path and trying to use the path if they were aggro'd to an enemy (#2005) * fix: enemies snapping to the incorrect position if they had a path tested that ags enemies no longer snap backwards a large amount * fix: move the home point so we can aggro correctly --- dGame/dComponents/MovementAIComponent.cpp | 60 ++++++++++++----------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index e885b4bb..c02d3a00 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -37,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<BaseCombatAIComponent>(); //Try and fix the insane values: @@ -131,7 +129,11 @@ 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 we're idle, that way we can return to this + // when resuming the pathing after losing aggro while moving the aggro hitbox with us + if (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle) m_BaseCombatAI->SetStartingPosition(approxPos); if (m_TimeTravelled < m_TimeToTravel) return; m_TimeTravelled = 0.0f; @@ -166,32 +168,34 @@ 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; - 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<PathWaypoint> 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. - if (m_BaseCombatAI) m_BaseCombatAI->SetStartingPosition(m_SourcePosition); + // Only try to renew or continue the path if we're in the idle or spawn state and we actually have a combatAI component + if (!m_BaseCombatAI || (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle)) { + // Check if there are more waypoints in the queue, if so set our next destination to the next waypoint + 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<PathWaypoint> 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(); return; } } else { - Stop(); - return; - } - } else { - SetDestination(m_CurrentPath.top().position); + SetDestination(m_CurrentPath.top().position); - m_CurrentPath.pop(); + m_CurrentPath.pop(); + } } } @@ -218,8 +222,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; @@ -473,7 +476,7 @@ void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) { break; } case eWaypointCommandType::DELAY: { - // Pause(GeneralUtils::TryParse<float>(data).value_or(0.0f)); + // Pause(GeneralUtils::TryParse<float>(data).value_or(0.0f)); break; } case eWaypointCommandType::EMOTE: { @@ -521,12 +524,13 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf movementInfo.PushDebug<AMFBoolValue>("Lock Rotation") = m_LockRotation; movementInfo.PushDebug<AMFBoolValue>("Paused") = m_Paused; movementInfo.PushDebug<AMFDoubleValue>("Pulling To Point") = m_PullingToPoint; + movementInfo.PushDebug<AMFBoolValue>("At Final Waypoint") = m_AtFinalWaypoint; auto& pullPointInfo = movementInfo.PushDebug("Pull Point"); pullPointInfo.PushDebug<AMFDoubleValue>("X") = m_PullPoint.x; pullPointInfo.PushDebug<AMFDoubleValue>("Y") = m_PullPoint.y; pullPointInfo.PushDebug<AMFDoubleValue>("Z") = m_PullPoint.z; - + // movementInfo.PushDebug<AMFDoubleValue>("Delay") = m_Delay; auto& waypoints = movementInfo.PushDebug("Interpolated Waypoints"); From 7456d6b5c11ea27c45f65f76d9f7fc2d6ef2cae8 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:08:15 -0700 Subject: [PATCH 64/69] feat: spawner weights (#2006) * feat: spawner weights * remove ref * default weights to 1 --- dCommon/LDFFormat.h | 8 ++ .../Map/General/VisToggleNotifierServer.cpp | 2 +- dZoneManager/Spawner.cpp | 74 ++++++++++--------- dZoneManager/Spawner.h | 46 +++++++++--- dZoneManager/Zone.cpp | 17 +++-- 5 files changed, 94 insertions(+), 53 deletions(-) diff --git a/dCommon/LDFFormat.h b/dCommon/LDFFormat.h index 81a6648c..0f515d30 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/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp b/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp index a02856ef..16d805eb 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/dZoneManager/Spawner.cpp b/dZoneManager/Spawner.cpp index cccdfff0..e7045842 100644 --- a/dZoneManager/Spawner.cpp +++ b/dZoneManager/Spawner.cpp @@ -6,8 +6,10 @@ #include <functional> #include "GeneralUtils.h" #include "dZoneManager.h" +#include <algorithm> +#include <ranges> -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<SpawnerNode*> freeNodes; for (SpawnerNode* node : m_Info.nodes) { @@ -77,9 +75,25 @@ Entity* Spawner::Spawn() { return Spawn(freeNodes); } -Entity* Spawner::Spawn(std::vector<SpawnerNode*> freeNodes, const bool force) { - if (force || ((m_Entities.size() < m_Info.amountMaintained) && (freeNodes.size() > 0) && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) { - SpawnerNode* spawnNode = freeNodes[GeneralUtils::GenerateRandomNumber<int>(0, freeNodes.size() - 1)]; +Entity* Spawner::Spawn(const std::vector<SpawnerNode*>& 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<int32_t>(1, spawnWeight); + + // Default to 0 incase something goes wrong in this calc + // Roll the spawner nodes based on their weights, higher weights = more likely to spawn + SpawnerNode* spawnNode = freeNodes[0]; + for (auto* const node : freeNodes) { + chosenWeight -= node->weight; + if (chosenWeight <= 0) { + spawnNode = node; + break; // we rolled a spawner + } + } + ++m_AmountSpawned; m_EntityInfo.pos = spawnNode->position; m_EntityInfo.rot = spawnNode->rotation; @@ -93,26 +107,24 @@ Entity* Spawner::Spawn(std::vector<SpawnerNode*> freeNodes, const bool force) { m_EntityInfo.spawnerID = m_Info.spawnerID; } - 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<void()> 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<LWOOBJID> Spawner::GetSpawnedObjectIDs() const { +const std::vector<LWOOBJID> Spawner::GetSpawnedObjectIDs() const { std::vector<LWOOBJID> 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<void()> 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 3f01161d..a5c9f355 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<LWOOBJID> 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<SpawnerNode*> freeNodes, bool force = false); + Entity* Spawn(const std::vector<SpawnerNode*>& freeNodes, bool force = false); void Update(float deltaTime); void NotifyOfEntityDeath(const LWOOBJID& objectID); void Activate(); @@ -57,16 +85,16 @@ public: int32_t GetAmountSpawned() { return m_AmountSpawned; }; std::string GetName() { return m_Info.name; }; std::vector<std::string> GetGroups() { return m_Info.groups; }; - void AddSpawnedEntityDieCallback(std::function<void()> callback); - void AddEntitySpawnedCallback(std::function<void(Entity*)> callback); - void SetSpawnLot(LOT lot); + void AddSpawnedEntityDieCallback(const std::function<void()> callback); + void AddEntitySpawnedCallback(const std::function<void(Entity*)> 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<LWOOBJID> GetSpawnedObjectIDs() const; + const std::vector<LWOOBJID> GetSpawnedObjectIDs() const; SpawnerInfo m_Info; bool m_Active = true; diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index fcb755f0..a5a8fa97 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -123,22 +123,25 @@ void Zone::LoadZoneIntoMemory() { 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; From a1891955e27450adaca611267673207762b70dc2 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:50:49 -0700 Subject: [PATCH 65/69] fix: leave team when fully logged out (#2007) * feat: spawner weights * remove ref * default weights to 1 * fix: remove team member if they've logged out tested that if i logout, after 20 seconds the team member is removed. --- dChatServer/PlayerContainer.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index 148cab2a..56fc8974 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); From 135aec8112d2aab24ff70785255be6e8eb9e16c8 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:10:00 -0700 Subject: [PATCH 66/69] feat: hatchlings (#2008) tested that hatchlings now function fixes #759 Update MovementAIComponent.cpp Update MovementAIComponent.cpp Update HatchlingPets.cpp --- dGame/Entity.cpp | 1 + dGame/dComponents/MovementAIComponent.cpp | 29 +++++++ dGame/dComponents/MovementAIComponent.h | 14 ++-- dScripts/02_server/CMakeLists.txt | 1 + dScripts/02_server/Objects/CMakeLists.txt | 11 ++- .../Objects/Hatchlings/CMakeLists.txt | 3 + .../Objects/Hatchlings/HatchlingPets.cpp | 83 +++++++++++++++++++ .../Objects/Hatchlings/HatchlingPets.h | 14 ++++ dScripts/CppScripts.cpp | 2 + 9 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 dScripts/02_server/Objects/Hatchlings/CMakeLists.txt create mode 100644 dScripts/02_server/Objects/Hatchlings/HatchlingPets.cpp create mode 100644 dScripts/02_server/Objects/Hatchlings/HatchlingPets.h diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index c4265470..bad48b71 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -2237,6 +2237,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& r objectInfo.PushDebug<AMFIntValue>("Template ID(LOT)") = GetLOT(); objectInfo.PushDebug<AMFStringValue>("Object ID") = std::to_string(GetObjectID()); objectInfo.PushDebug<AMFStringValue>("Spawner's Object ID") = std::to_string(GetSpawnerID()); + objectInfo.PushDebug<AMFStringValue>("Owner override") = std::to_string(m_OwnerOverride); auto& componentDetails = objectInfo.PushDebug("Component Information"); for (const auto [id, component] : m_Components) { diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index c02d3a00..8a10df25 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -189,6 +189,15 @@ void MovementAIComponent::Update(const float deltaTime) { } } 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 { @@ -555,5 +564,25 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf pathCopy.pop(); } + movementInfo.PushDebug<AMFStringValue>("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 af7da7f0..eeb871cf 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{}; }; /** @@ -214,6 +214,8 @@ public: bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); bool HasPath() const { return m_Path != nullptr; } + + void FollowTarget(const LWOOBJID target); private: /** @@ -337,6 +339,8 @@ private: // 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/dScripts/02_server/CMakeLists.txt b/dScripts/02_server/CMakeLists.txt index 8114b226..048139a0 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/Objects/CMakeLists.txt b/dScripts/02_server/Objects/CMakeLists.txt index 1b96d79f..da7e9b60 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 00000000..0655ec64 --- /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 00000000..bc29ae8d --- /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<MovementAIComponent>(-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<bool>(u"follow")) { + const auto randomWanderTime = GeneralUtils::GenerateRandomNumber<float>(4, 9); + self->AddTimer("StartWander", randomWanderTime); + // stop following the player + auto* const movementAI = self->GetComponent<MovementAIComponent>(); + 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<MovementAIComponent>(); + 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<MovementAIComponent>(); + 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<float>(0, 20) - 10.0f; + const auto zWander = GeneralUtils::GenerateRandomNumber<float>(0, 20) - 10.0f; + getPos.pos.x += xWander; + getPos.pos.z += zWander; + auto* const movementAI = self.GetComponent<MovementAIComponent>(); + if (movementAI) movementAI->SetDestination(getPos.pos); + self.AddTimer("StartWander", GeneralUtils::GenerateRandomNumber<float>(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 00000000..0dd2d5b6 --- /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/CppScripts.cpp b/dScripts/CppScripts.cpp index a1f37153..fc955819 100644 --- a/dScripts/CppScripts.cpp +++ b/dScripts/CppScripts.cpp @@ -166,6 +166,7 @@ #include "AgSalutingNpcs.h" #include "BossSpiderQueenEnemyServer.h" #include "RockHydrantSmashable.h" +#include "HatchlingPets.h" // Misc Scripts #include "ExplodingAsset.h" @@ -423,6 +424,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();}}, From ccad52a5cea2c68291f9b77f31b993ce775cc8cd Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:10:13 -0700 Subject: [PATCH 67/69] feat: testmap improvements (#2009) * feat: testmap improvements fixes #1191 removed the force flag because it would only work to let you softlock your character. tested that taking the lego club door now spawns you at the lego club/ns lego club doors always vs letting you spawn where ever. tested that a testmap no longer spawns you where you last were in a zone and instead spawns you at the spawn point inspect allows you to inspect zoneControl and localCharacter now updated docs with the new info * Update Entity.cpp --- dGame/Entity.cpp | 7 +-- dGame/dUtilities/SlashCommandHandler.cpp | 4 +- .../SlashCommands/DEVGMCommands.cpp | 45 ++++++++++++------- dScripts/BaseConsoleTeleportServer.cpp | 3 ++ docs/Commands.md | 4 +- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index bad48b71..daffec1b 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(); diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index 6c2d96ee..e4782db4 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 745798b8..5b464ef0 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -1051,7 +1051,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<LWOMAPID>(splitArgs[0]); if (!reqZoneOptional) { @@ -1061,29 +1062,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<LWOCLONEID>(splitArgs[1]); + if (!cloneIdOptional) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id."); + return; } - if (splitArgs.size() > index) { - const auto cloneIdOptional = GeneralUtils::TryParse<LWOCLONEID>(splitArgs[index]); - if (!cloneIdOptional) { - ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id."); + cloneId = cloneIdOptional.value(); + + if (splitArgs.size() > 2) { + const auto instanceIDVal = GeneralUtils::TryParse<LWOINSTANCEID>(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; @@ -1101,6 +1107,7 @@ namespace DEVGMCommands { entity->GetCharacter()->SetZoneID(zoneID); entity->GetCharacter()->SetZoneInstance(zoneInstance); entity->GetCharacter()->SetZoneClone(zoneClone); + entity->GetCharacter()->SetTargetScene(targetScene); entity->GetComponent<CharacterComponent>()->SetLastRocketConfig(u""); } @@ -1513,7 +1520,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<LWOOBJID>(splitArgs[0]); + std::optional<LWOOBJID> idIntermed; + if (splitArgs[0] == "zoneControl") { + idIntermed = 0x3FFF'FFFFFFFE; + } else if (splitArgs[0] == "localCharacter") { + idIntermed = entity->GetObjectID(); + } else { + idIntermed = GeneralUtils::TryParse<LWOOBJID>(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/BaseConsoleTeleportServer.cpp b/dScripts/BaseConsoleTeleportServer.cpp index d172696c..7392f759 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<std::u16string>(u"transferZoneID"); + auto* const character = player->GetCharacter(); + if (character && self->HasVar(u"spawnPoint")) character->SetTargetScene(self->GetVarAsString(u"spawnPoint")); + auto* characterComponent = player->GetComponent<CharacterComponent>(); if (characterComponent) characterComponent->SendToZone(GeneralUtils::TryParse(GeneralUtils::UTF16ToWTF8(teleportZone), 0)); diff --git a/docs/Commands.md b/docs/Commands.md index 702f64cc..2d93adeb 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 <body part> <minifig item id>`|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 <zone> (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 <zone> (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 <x/source player> (y) <z/target player>`|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 <uscore>`|Gives uscore|8| |gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id. Aliases: `/give`.|8| -|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> | -a <animation> | -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 <component or ldf variable or player name> (-m <waypoint> | -a <animation> | -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| |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| |lookup|`/lookup <query>`|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8| From ccc029424c96fca64a8a7cbc7521c041992a6ee2 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:53:49 -0700 Subject: [PATCH 68/69] fix: fv mpc in the tree (#2010) tested that it now spawns in the lower path point instead. fixes #741 --- dGame/Entity.cpp | 15 ++++++++------- dGame/dGameMessages/GameMessages.cpp | 15 ++++++++------- dGame/dGameMessages/GameMessages.h | 4 +++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index daffec1b..069d984a 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -613,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<int32_t>(u"activityID"); if (activityID > 0) { @@ -1616,7 +1617,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(); } diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 6b57e177..0acb4452 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()); @@ -6484,8 +6485,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 eef568d3..7a34de6c 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -103,9 +103,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); From 83707e221032bf5548d15906fb267494dd224a04 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:30:44 -0700 Subject: [PATCH 69/69] feat: add the lightning orb script* (#2011) * feat: add the lightning orb script* Doesn't do anything because MPC don't work... * Update LightningOrbServer.cpp --- dCommon/Amf3.h | 15 ++++++++++++++- dScripts/02_server/Map/njhub/CMakeLists.txt | 1 + .../02_server/Map/njhub/LightningOrbServer.cpp | 12 ++++++++++++ dScripts/02_server/Map/njhub/LightningOrbServer.h | 8 ++++++++ dScripts/CppScripts.cpp | 2 ++ 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 dScripts/02_server/Map/njhub/LightningOrbServer.cpp create mode 100644 dScripts/02_server/Map/njhub/LightningOrbServer.h diff --git a/dCommon/Amf3.h b/dCommon/Amf3.h index 174ad814..689e7607 100644 --- a/dCommon/Amf3.h +++ b/dCommon/Amf3.h @@ -369,8 +369,21 @@ public: template<typename AmfType = AMFArrayValue> AmfType& PushDebug(const std::string_view name) { + size_t i = 0; + for (; i < m_Dense.size(); i++) { + const auto& cast = dynamic_cast<AMFArrayValue*>(m_Dense[i].get()); + if (!cast) continue; + + const auto& nameValue = cast->Get<std::string>("name"); + if (!nameValue || nameValue->GetValue() != name) continue; + + // found a duplicate, return this instead + auto valueCast = dynamic_cast<AmfType*>(cast->Get("value")); + if (valueCast) return *valueCast; + } + auto* value = PushArray(); - value->Insert("name", name.data()); + value->Insert<std::string>("name", name.data()); return value->Insert<AmfType>("value", std::make_unique<AmfType>()); } diff --git a/dScripts/02_server/Map/njhub/CMakeLists.txt b/dScripts/02_server/Map/njhub/CMakeLists.txt index 367d4647..b2d0ba7f 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 00000000..e686d540 --- /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 00000000..962d7b6a --- /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/CppScripts.cpp b/dScripts/CppScripts.cpp index fc955819..a6c3c00d 100644 --- a/dScripts/CppScripts.cpp +++ b/dScripts/CppScripts.cpp @@ -274,6 +274,7 @@ #include "MonCoreNookDoors.h" #include "MonCoreSmashableDoors.h" #include "FlameJetServer.h" +#include "LightningOrbServer.h" #include "BurningTile.h" #include "NjEarthDragonPetServer.h" #include "NjEarthPetServer.h" @@ -628,6 +629,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();}},