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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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);