From 0253fea006e7f5296dfaad357aa6776e8097caf9 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Fri, 2 Oct 2026 09:31:35 -0500 Subject: [PATCH] feat(destroyable): enemy preconditions from the level, as the client An object's EnemyPrecondition (level config) decides who it is an enemy of: whoever asks and doesn't meet the preconditions, whatever the factions (LWODestroyableComponent::msgIsEnemy 0x00c9e930; ',' ';' ':' join groups that must all pass, '|' alternatives). Forbidden Valley Siege's objects use it today; any level object can. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/DestroyableComponent.cpp | 8 ++++++++ dGame/dComponents/DestroyableComponent.h | 10 ++++++++++ .../DestroyableComponentTests.cpp | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 51fcf0366..30781567c 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -28,6 +28,7 @@ #include "BaseCombatAIComponent.h" #include "TeamManager.h" #include "BuffComponent.h" +#include "Preconditions.h" #include "SkillComponent.h" #include "Item.h" #include "Amf3.h" @@ -163,6 +164,9 @@ void DestroyableComponent::LoadDataFromTemplate() { // The level config (LWODestroyableComponent::LoadConfigData 0x00c44cb0 reads it for every destroyable): is_smashable, // set_faction / override_faction and the smashable loot matrix void DestroyableComponent::LoadConfigData() { + const auto enemyPrecondition = m_Parent->GetVarAsString(u"EnemyPrecondition"); + if (!enemyPrecondition.empty()) m_EnemyPrecondition = std::make_unique(enemyPrecondition); + const bool isSmashable = m_Parent->GetVarAs(u"is_smashable") != 0; const bool isCharacter = m_Parent->GetCharacter() != nullptr; if (!isCharacter) { @@ -536,6 +540,10 @@ bool DestroyableComponent::IsEnemy(const Entity* other) const { return false; } const auto* otherDestroyableComponent = other->GetComponent(); + // An object with enemy preconditions is an enemy unless this one meets them; factions don't count then + if (otherDestroyableComponent && otherDestroyableComponent->GetEnemyPrecondition()) { + return !otherDestroyableComponent->GetEnemyPrecondition()->Check(m_Parent); + } if (otherDestroyableComponent != nullptr) { for (const auto enemyFaction : m_EnemyFactionIDs) { for (const auto otherFaction : otherDestroyableComponent->GetFactionIDs()) { diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index ab5c5ae03..c4c6834ba 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -2,6 +2,7 @@ #define DESTROYABLECOMPONENT_H #include "RakNetTypes.h" +#include #include #include #include "tinyxml2.h" @@ -10,6 +11,8 @@ #include "eReplicaComponentType.h" #include "Implementation.h" +class PreconditionExpression; + struct ObjectLoadState; namespace GameMessages { @@ -471,6 +474,10 @@ public: */ static void Resurrect(Entity& entity); + // The level's EnemyPrecondition: an object asking whether this one is its enemy must meet it, or it is one + // (LWODestroyableComponent::msgIsEnemy 0x00c9e930); its factions don't count then + const PreconditionExpression* GetEnemyPrecondition() const { return m_EnemyPrecondition.get(); } + // Whether a resurrection has started and not finished (the client's isResurrecting) bool GetIsResurrecting() const { return m_IsResurrecting; } @@ -640,6 +647,9 @@ private: // A resurrection has started (Resurrect) and its respawn animation hasn't finished bool m_IsResurrecting = false; + // The level config's EnemyPrecondition, when it has one + std::unique_ptr m_EnemyPrecondition; + /** * Whether this entity is smashed. Unused, here for serialization */ diff --git a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp index 27d7829a0..b3600d867 100644 --- a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp @@ -731,3 +731,23 @@ TEST_F(DestroyableTest, RequestResurrectOnlyWhenDead) { request.Handle(*baseEntity, UNASSIGNED_SYSTEM_ADDRESS); EXPECT_TRUE(destroyableComponent->GetIsResurrecting()); } + +// LWODestroyableComponent::msgIsEnemy: a target with enemy preconditions is an enemy of whoever doesn't meet them, +// whatever its factions +TEST_F(DestroyableTest, EnemyPreconditionDecidesEnemies) { + if (!CDClientDatabase::isConnected) CDClientDatabase::Connect(":memory:"); + CDClientDatabase::ExecuteDML("CREATE TABLE IF NOT EXISTS Preconditions (id INTEGER, type INTEGER, targetLOT TEXT, targetCount INTEGER);"); + CDClientDatabase::ExecuteDML("INSERT INTO Preconditions VALUES (90201, 2, '6194', 1);"); // has an item the asker lacks + CDClientDatabase::ExecuteDML("INSERT INTO Preconditions VALUES (90202, 2, '', 1);"); // always met + + Entity target(0x0102030405060709LL, info); + auto* targetDestroyable = target.AddComponent(-1); + + target.SetVar(u"EnemyPrecondition", "90201"); + targetDestroyable->LoadConfigData(); + EXPECT_TRUE(destroyableComponent->IsEnemy(&target)); + + target.SetVar(u"EnemyPrecondition", "90202"); + targetDestroyable->LoadConfigData(); + EXPECT_FALSE(destroyableComponent->IsEnemy(&target)); +}